Webhooks vs polling: choosing how services tell each other things
Two services need to stay in sync. Either the interested party asks repeatedly whether anything has changed, or the source calls a URL when something does. Polling is a request you control; a webhook is a request you receive. The comparison is usually framed as efficiency, webhooks win, but that framing hides the actual tradeoff, which is where the complexity ends up. Polling puts it in your control loop. Webhooks move it into a public endpoint, delivery guarantees, and failure handling.
The comparison
| Polling | Webhooks | |
|---|---|---|
| Who initiates | You | The provider |
| Latency | Up to the poll interval | Near immediate |
| Wasted requests | Many, most returning nothing | None |
| Public endpoint required | No | Yes |
| Missed events | Impossible, you re read | Possible, needs retries |
| Ordering | You control it | Not guaranteed |
| Duplicate delivery | Not an issue | Expected |
| Backpressure | Natural, you set the rate | None, you absorb spikes |
| Local development | Trivial | Needs a tunnel |
| Debugging | Rerun the request | Inspect what arrived |
Read the "missed events" and "backpressure" rows together and the real tradeoff appears. Polling is a pull model, so you can never fall behind in a way that loses data: if your worker was down for an hour, the next poll simply returns more. Webhooks are a push model, so an outage on your side means events that were delivered to nothing, and a spike on the provider's side arrives at whatever rate they send it.
What a webhook consumer actually has to handle
The endpoint is five lines. The correctness around it is not.
Verify the signature. Your webhook URL is a public HTTP endpoint that anyone can find and call. Without verification, anyone can post a fabricated "payment succeeded" event. Providers sign the raw request body with a shared secret, usually HMAC-SHA256, and send the result in a header. Compute the same signature over the raw bytes, before any JSON parsing or reserialisation, and compare in constant time. Include the timestamp the provider sends in the signed payload and reject anything older than a few minutes, otherwise a captured request can be replayed indefinitely. The mechanics are covered in verify webhook signatures with HMAC, and you can check your implementation against a known input with the HMAC generator.
Respond fast, process later. Providers time out, commonly at 5 to 30 seconds, and count a timeout as a failure. Validate, enqueue, and return 200 immediately; do the real work in a background job. An endpoint that processes inline will eventually be slow enough to trigger retries, which arrive as duplicates and make the load worse.
Expect duplicates. Delivery is at least once. A network blip after your handler committed but before the response arrived produces a retry of an event you already processed. Deduplicate on the provider's event ID, store the IDs you have handled, and make the handler idempotent so a repeat is a no op rather than a second charge.
Do not assume ordering. Retries and parallel delivery mean an updated event can arrive before the created it followed. Either carry a version or timestamp and ignore stale updates, or treat the payload as a hint and fetch the current state from the provider's API, which is the more robust pattern.
Return the right status codes. 2xx means delivered. Any other status is a failure that will be retried on the provider's schedule, typically exponential backoff over hours or days. That means returning 500 for a malformed payload you will never accept causes days of pointless retries, and can get your endpoint disabled. Return 200 for events you have deliberately decided to drop, and reserve failure codes for genuine transient problems. The distinction between client and server errors is in HTTP status codes explained.
Plan for downtime. Retries eventually stop. Most providers also expose an events API listing what they sent, and a reconciliation job that replays anything you missed is the difference between a robust integration and one that silently loses data during an incident.
When polling is the better choice
Polling is unfashionable and frequently correct.
- The data is not urgent. A nightly sync does not benefit from sub second delivery.
- You cannot expose a public endpoint. Behind a firewall, in a desktop application, or in an environment where inbound traffic is not permitted.
- The provider's webhooks are unreliable, which is more common than provider documentation suggests.
- You need strict ordering or exactly once processing. Reading a cursor based feed in order gives you both, for free.
- Volume is low. Polling a handful of endpoints every few minutes costs almost nothing, and the operational simplicity is worth more than the saved requests.
Polling well means using the API's efficiency features rather than refetching everything: conditional requests with If-None-Match returning 304 Not Modified, an updated_since filter, or a cursor you advance. The relevant headers are covered in cache control headers explained. Add jitter to the interval so a fleet of workers does not synchronize into a spike, and honour 429 and Retry-After rather than retrying immediately.
The combination most mature integrations reach
Webhooks as a signal, polling as the safety net. The webhook tells you something changed and you fetch the authoritative state from the API; a periodic reconciliation job catches anything the webhook missed. This removes three problems at once. Ordering stops mattering, because you always read current state. Payload trust matters less, since the API is the source of truth. And a delivery outage becomes a delay rather than data loss.
It also avoids a subtle security issue with acting directly on payload contents: an event says an invoice was paid, but the amount and status you act on came from a request body rather than from the provider. Signature verification makes forgery hard; fetching the record makes it irrelevant.
Developing and debugging
The awkward part of webhooks is that they are inbound, so a local server cannot receive them without a tunnel. Two approaches make this manageable:
- Capture a real delivery first. Point the provider at a disposable endpoint, trigger the event, and read exactly what arrives, headers, signature, and body, with the webhook inspector. Knowing the real shape before writing the handler saves most of the guesswork, and provider documentation is often out of date on the details.
- Replay locally. Once you have a captured payload, send it to your local handler with the API Tester or curl. Note that a replayed body will fail signature verification unless you also copy the signature header and keep the body byte identical, which is itself a useful test that your verification works.
How to debug webhooks walks through the common failures: signature mismatches caused by body reserialisation, timeouts from inline processing, and events silently dropped by a 4xx returned during a deploy.