Webhooks vs Polling: Pushing Data Instead of Pulling
Polling wastes 99% of its requests asking a question whose answer is 'nothing new.' Webhooks flip the direction and eliminate that waste — while handing you a public endpoint, unverified callers, duplicate deliveries, and failures you can't see. Here's the honest comparison.
The 99% You're Throwing Away
You integrate with a payment provider. You need to know when a charge settles, so every 30 seconds you ask: any updates? The answer, almost always, is no.
At one request per 30 seconds per resource, watching 10,000 charges means 20,000 requests a minute, of which perhaps a hundred return anything. You're burning rate limit, bandwidth, and your provider's capacity to be told "nothing happened," over and over. And despite all that traffic, you still learn about each settlement up to 30 seconds late.
Both problems have the same cause: the side that knows when something happened isn't the side doing the asking. A webhook fixes the direction — the provider calls you the moment the charge settles. Zero wasted requests, near-zero latency.
It also makes you operate a public HTTP endpoint that strangers can call, which is where the interesting engineering starts.
The Two Directions
A user-defined HTTP callback. You register a URL with a provider; when an event occurs, the provider sends an HTTP POST to that URL with the event payload. Control is inverted — instead of you calling their API, their system calls yours. Sometimes called a reverse API, and the mental shift is exactly that: your integration point is now a server, not a client.
| Polling | Webhooks | |
|---|---|---|
| Latency | Up to your poll interval | Near real-time |
| Wasted requests | Most of them | None |
| Who must be reachable | The provider (already public) | You — a public HTTPS endpoint |
| Failure visibility | Obvious — your request errors | Silent — you don't know what you missed |
| Delivery guarantee | You control it; retry whenever | At-least-once, at the provider's discretion |
| Ordering | You read in whatever order you like | Not guaranteed; retries arrive out of order |
| Auth burden | You authenticate to them | You must authenticate *them* |
| Backpressure | Natural — poll slower | None — bursts arrive at their pace |
| Works behind a firewall | Yes | No |
The row that decides most architectures is the last one. A background worker inside a private network can poll anything. Receiving a webhook means exposing an endpoint to the internet, and for a lot of internal systems that alone settles it.
What Receiving a Webhook Actually Requires
The naive handler — parse the JSON, do the work, return 200 — is wrong in four separate ways, each of which becomes a production incident.
Verify the signature before you trust anything
Your endpoint is a public URL. Anyone who guesses it can POST {"event": "payment.succeeded", "amount": 500000} at you. Providers sign each delivery — typically an HMAC-SHA256 of the raw request body plus a timestamp, using a shared secret, in a header like X-Signature. Recompute it over the raw bytes (not the re-serialized JSON — key order and whitespace will differ) and compare in constant time. Reject anything that fails, and reject old timestamps too, or a captured request can be replayed at you forever.
Return 200 fast, then do the work
Providers time out, often in 5 to 10 seconds, and a timeout counts as a failed delivery that gets retried. If your handler charges a card, sends an email, and updates three tables inline, a slow moment turns into a duplicate delivery — and now you've done the side effects twice. Persist the raw event, return 200, and process asynchronously from a queue. The endpoint's only job is to accept and durably record.
Deduplicate — delivery is at-least-once
Every webhook system will eventually send you the same event twice: a retry after your 200 was lost in transit, a provider-side redelivery, a network hiccup. Each event carries a stable id. Store processed ids with a unique constraint and drop repeats. Without this you send two confirmation emails, or credit an account twice, on a day that looked completely normal from your side.
Don't trust the order, and don't trust the payload's freshness
Retries mean order.updated can arrive after order.completed. Treat each event as a signal that something changed and, for anything important, fetch the current state from the provider's API rather than acting on the body alone. Where the payload is authoritative, carry a version or timestamp and ignore anything older than what you've already applied.
raw = request.body_bytes # raw, not re-serialized
if not hmac_equal(sign(raw, SECRET), header["X-Signature"]):
return 401
if now() - header["X-Timestamp"] > 300: # 5 min replay window
return 401
event = parse(raw)
# Unique index on event_id makes the duplicate a no-op, not a bug.
inserted = db.insert_if_absent("webhook_events", event.id, raw)
if inserted:
queue.publish("process_webhook", event.id)
return 200 # under 100ms, always⚠️ A 200 you return by accident is a delivery you never get again
Providers stop retrying after a handful of attempts, and some disable an endpoint entirely after sustained failures. Two consequences: never return 200 for an event you haven't durably stored, and never return 200 for one you rejected — the retry is your safety net, and a wrongly-successful response throws it away. Equally, never return 500 for a permanently-invalid payload; that just guarantees retries until the provider gives up on you.
The Failure Mode Polling Doesn't Have
When polling breaks, you notice: your requests error, your logs fill, your alerts fire. When webhooks break, everything looks fine. Your endpoint returns 200 to zero requests. There's no error to log, because a delivery that was never attempted — or was attempted while you were deploying and returned 502 five times before the provider gave up — leaves no trace on your side.
You silently stop learning about events. Sometimes for days, until a customer asks why their subscription is still marked unpaid.
🔴 Every serious webhook integration needs a reconciliation poll
Run a low-frequency sweep — hourly, daily — that lists recent events from the provider's API and checks them against what you processed. It catches missed deliveries, gaps during outages, and events you dropped while a bug was live. This isn't an admission that webhooks failed; it's the standard design. Webhooks give you low latency, the reconciliation sweep gives you completeness, and you need both. Also alert on the absence of events: if a normally-busy endpoint receives nothing for an hour, something is wrong.
Providers help here to varying degrees — an events API you can list and page through, a dashboard showing delivery attempts and responses, a manual redelivery button. When evaluating an integration, those observability features matter more than the payload schema.
The Other Options
Webhooks and polling aren't the only two directions. Two more are worth knowing, because they beat both in specific situations.
| Approach | Direction | Best for | Watch out for |
|---|---|---|---|
| Polling | You pull, on a timer | Simple integrations, firewalled systems, low event rates | Wasted requests; latency bounded by interval |
| Long polling | You pull, server holds the request open | Near-real-time without a public endpoint | Held connections; timeouts and reconnect logic |
| Webhooks | They push to your HTTPS endpoint | Server-to-server, low latency, high fanout | Public endpoint; signature, dedup, silent failure |
| Streaming (SSE / WebSocket) | Persistent connection, they push | Continuous high-rate updates, browser clients | Connection state; reconnect and resume from a cursor |
Long polling is the underrated middle. You make a request, the server holds it open until an event happens or a timeout expires, then you immediately request again. You get push-like latency while remaining a client — no public endpoint, no signature verification, works from inside a private network. It costs a held connection per watcher, which is fine for tens of clients and untenable for tens of thousands.
✅ For a busy resource, polling stops being wasteful
The efficiency argument for webhooks assumes most polls return nothing. Invert that: if a resource changes several times a second, a poll every second returns a useful batch every time, and it gives you natural backpressure and ordering that webhooks don't. Polling is wasteful when events are rare, not in general. Do the arithmetic — events per interval — before assuming push is the efficient choice.
Choosing
Can your service expose a public HTTPS endpoint?
Key Takeaways
- Polling wastes requests because the wrong side is asking. Webhooks invert control so the system that knows about the event initiates the call.
- A webhook endpoint is a public API you now operate. Verify the HMAC signature over raw bytes, in constant time, with a timestamp window to stop replays.
- Accept fast, process async. Store the raw event, return 200 in milliseconds, and do the real work from a queue — provider timeouts become duplicate deliveries.
- Delivery is at-least-once and unordered. Deduplicate on event id with a unique constraint, and fetch current state rather than trusting a possibly-stale payload's ordering.
- The dangerous failure is silence. A webhook that never arrives leaves no trace. Run a reconciliation poll and alert on an absence of events.
- Never 200 an event you haven't stored, never 500 one that's permanently invalid. The retry mechanism is your safety net; both mistakes disable it.
- Polling isn't the naive choice — it wins for firewalled systems, for high-frequency resources where every poll returns a batch, and any time you want ordering and backpressure for free.
References
- Stripe — Webhook signature verification and best practices — the reference implementation most providers imitate
- GitHub — Securing your webhooks — HMAC validation, raw-body handling, and constant-time comparison
- Standard Webhooks specification — an emerging cross-provider convention for signatures, ids, and retries
- Idempotency: Why POST Should Sometimes Act Like PUT — the deduplication problem in its general form