Library
8 min read

Webhooks vs polling

Published

You are waiting for a courier. Polling is calling the courier company every ten minutes to ask "has it left yet?". A webhook is giving them your number and asking them to call you when it leaves. The second is obviously better, until the phone is switched off when they call, or they call twice, or someone else rings pretending to be the courier.

Between services, a webhook is an HTTP POST that a provider sends to a URL you registered, when an event happens on their side: Razorpay telling you a payment succeeded, GitHub telling you someone pushed, Shopify telling you an order was placed.

Your answer

Your app receives Razorpay's 'payment.captured' webhook and marks the order paid. List everything that could go wrong between Razorpay sending it and your database being correct.

What polling costs#

Polling is simple and in your control, and it is wasteful in exact proportion to how rarely things change.

Polling a payment provider for status
5000
20
30 s
Poll requests per day5,000 x 86,400 / 30 = 1,44,00,000
Events that actually happened1,00,000
Polls that found nothing99.3%
Average delay before you notice15 s
99.3%
of polling requests wasted

Shorter intervals cut the delay and multiply the waste, and the provider will rate limit you long before you get to real time. Webhooks send exactly one request per event, immediately.

Webhooks, and the five rules for receiving them#

A webhook is efficient and immediate. It is also an incoming request from the internet that affects your money, arriving at least once, possibly out of order, and possibly not at all if your endpoint was down long enough. Receiving them correctly means five habits.

1/8 Signed with HMAC SHA-256 using a secret only you and Razorpay know.
  1. Verify the signature on the raw request body with the shared secret, using a constant time compare. Check the timestamp included in the signature (Stripe's Stripe-Signature header has one) and reject old ones, so a captured request cannot be replayed later.
  2. Acknowledge fast, process later. Return 2xx as soon as the event is safely stored or queued. Slow handlers cause timeouts, and timeouts cause retries.
  3. Deduplicate by event id. Providers retry on any failure or timeout, so the same event can arrive twice. See delivery guarantees.
  4. Do not trust order. "payment.refunded" can arrive before "payment.captured". Either compare timestamps or versions, or fetch the object's current state from the API when an event arrives.
  5. Reconcile. If your endpoint was down for longer than the provider retries (Stripe retries for up to three days, others less), events are lost. A periodic job that lists recent objects from the API and fixes any mismatch is the safety net. Webhooks for speed, polling for correctness.
ts
import { createHmac, timingSafeEqual } from 'node:crypto'
 
// Use the raw body bytes, exactly as received. Parsing and re-serialising
// JSON changes whitespace and breaks the signature.
export function verify(rawBody: Buffer, header: string, secret: string) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(header)
  return a.length === b.length && timingSafeEqual(a, b)
}

Sending webhooks#

If you are the provider, the mirror image applies: sign every payload with a per customer secret and include a timestamp; send from a queue with retries and exponential backoff over hours or days; give each event a unique id; let customers see delivery attempts and replay events from a dashboard; disable endpoints that fail for days and email the owner. And do not let one customer's slow endpoint hold up everyone else's deliveries: isolate per customer queues or concurrency.

ChoiceWhat you gainWhat you payPick it when
PollingSimple, works through firewalls, the client controls the pace, nothing to secure on your side.Wasted requests, delay of up to one interval, provider rate limits.Few objects to watch, slow changing data, environments that cannot receive inbound calls, and as reconciliation.
WebhooksImmediate, one request per event, no wasted calls.Public endpoint to secure, at least once and unordered delivery, lost events if down too long.Payments, Git events, order updates: anything event driven between companies.
Both: thin webhook + fetch + reconcileSpeed of webhooks, correctness of reading current state, safety net for lost events.More code paths.Anything where money or orders depend on it.
Names you will hear
  • StripeSigned webhooks with timestamps, 3 days of retries
  • RazorpayX-Razorpay-Signature, HMAC SHA-256
  • GitHubX-Hub-Signature-256, redelivery from the UI
  • SvSvix / HookdeckWebhook sending and receiving as a service

In the interview#

Interviewer
How does your system learn that a customer's payment succeeded?
A payments design classic. Expect follow ups on every failure mode.
You
Primarily through the gateway's webhook. The endpoint verifies the HMAC signature and timestamp on the raw body, writes the event to a queue and returns 200 immediately. A worker dedupes on the event id, fetches the payment's current state from the gateway API rather than trusting the event order, and updates the order in a transaction with the processed id. The app also polls our own order status while the user waits on the confirmation screen, and a reconciliation job every few minutes lists recent payments from the gateway to catch any webhook we missed.
All five rules, plus how the user's screen gets updated.
Webhook or polling?
Learning that a UPI payment succeeded, as fast as possible
An on premise server behind a corporate firewall checking for license updates
Triggering CI when code is pushed to GitHub
Checking a government API that offers no webhooks

Checkpoint#

Checkpoint

1. Why should a webhook endpoint return 200 quickly and process the event later?

2. Why verify the signature on the raw body rather than parsed JSON?

3. Your endpoint was down for four days. What protects you from lost events?

Worth memorising
  • Polling: you ask on a timer. Webhook: they call you on each event.
  • Verify HMAC on the raw body + timestamp.
  • Ack fast, process async, dedupe by event id.
  • Events are unordered: fetch current state.
  • Reconcile with polling. Webhooks for speed, polling for correctness.
Say this in 60 seconds

Polling asks a provider for changes on a timer: simple, under the client's control and firewall friendly, but most requests find nothing and updates arrive late by up to an interval. A webhook is an HTTP POST the provider sends to your URL when an event happens: immediate and efficient, but it is a public endpoint receiving at least once, unordered, and possibly lost deliveries. So I verify the HMAC signature on the raw body with a timestamp to block forgery and replay, return 200 as soon as the event is queued, deduplicate by event id in the same transaction as the effect, fetch the object's current state instead of trusting event order, and run a reconciliation job that polls the provider's API to catch anything missed. When sending webhooks, I sign them, retry with backoff from a queue, isolate slow customers, and let customers replay events.