Home › High-Level Design › Webhooks
W

Webhooks

When your system needs to tell someone else that something happened, a webhook is usually the cheapest, most scalable way to say it. I have shipped payment integrations, CI pipelines, and chat bots that all lean on webhooks, and in interviews this one topic cleanly separates people who have read about event-driven systems from people who have been paged at 3am because a webhook silently stopped firing. The whole topic lives in a handful of failure modes, so learn those and you can defend the design in your sleep.

High-Level DesignIntermediate10 min read

If you remember five things

The entire topic in one glance. Everything below is the defense of these five calls.

  1. Push on an event, not pull on a timer. A webhook is a reverse API call: you register a URL, the provider POSTs to it the instant an event fires. No traffic until there is something to say.
  2. Ack fast, process async. Verify, dedupe, enqueue, return 2xx in milliseconds. The real work happens in a worker. This is the single most important call in the whole design.
  3. At-least-once plus idempotency. You cannot get exactly-once over HTTP. You get at-least-once delivery, and you make processing idempotent so the effect is exactly-once.
  4. Verify the raw bytes. The URL is public, so recompute the HMAC over the raw body and reject stale timestamps. No verification means anyone can POST a fake payment.succeeded.
  5. You own reliability after the ack. Once you return 200, retries, dead-letter queues, and monitoring are yours. A silent 500 for an hour loses events with no alarm.

1The core idea#

A webhook flips the direction of a normal API call. Instead of your service calling someone and waiting, you register a URL and the provider calls you.

  • Normal API: your code asks Stripe "has this payment succeeded yet?" on a loop.
  • Webhook: Stripe calls your /webhooks/stripe endpoint the instant the charge clears and hands you the whole event.
  • The mental model: "don't call us, we'll call you" for software.

The alternative is polling, and the cost difference is brutal.

  • Polling is a consumer asking "anything new?" on a timer.
  • Say 50,000 clients each poll every 10 seconds for an event that fires once an hour.
  • That is 5,000 requests per second, and 99.99% of them return "nothing new."
  • Webhooks invert that: zero traffic until there is actually something to say, then one POST.

Polling is pull on a timer. Webhooks are push on an event.

2How it works#

The lifecycle has three phases. Keep them straight and the rest falls out.

  • Registration. The consumer tells the provider a callback URL and which events it cares about (say payment.succeeded and payment.refunded). Dashboard setting or API call. The provider stores it as a subscription.
  • Delivery. On a matching event, the provider serializes it (almost always JSON) and POSTs to the registered URL, with a signature header, an event ID, and usually a timestamp.
  • Handling. The consumer verifies the signature, does the minimum work to safely accept the event, returns a fast 2xx, and processes the real logic asynchronously.

Here is the delivery flow for a typical payment webhook:

sequenceDiagram participant U as User participant P as Payment Provider participant W as Your Webhook Endpoint participant Q as Queue participant S as Order Service U->>P: Completes payment P->>W: POST event with signature W->>W: Verify signature W->>Q: Enqueue event W-->>P: 200 OK Q->>S: Deliver job S->>S: Fulfill order
The endpoint returns 200 OK before the order is fulfilled. The ack and the real work are deliberately split so a slow fulfillment can never turn into a provider timeout.
The single most important choice

Return 2xx before you do the heavy lifting. Providers treat a slow or failed response as a delivery failure and retry. Stripe, for example, times out a delivery at 20 seconds and marks it failed. Do your database writes, email sends, and third-party calls inline and you will time out, the provider retries, and now you have a duplicate. Accept fast, process later.

The internal shape of a resilient receiver.

  • The receiver is thin: verify, dedupe, enqueue, ack.
  • The worker is where the real work happens.
  • Split this way, the worker scales, retries, and is monitored independently of the hot path.
flowchart LR A[Provider] --> B[Load Balancer] B --> C[Webhook Receiver] C --> D[(Dedup Store)] C --> E[Message Queue] E --> F[Worker] F --> G[(App Database)]
The seam that matters is between the receiver and the queue. Everything left of the queue is cheap and fast so the provider always sees a quick 200. Everything right of it is where the slow, failure-prone work lives.

3A worked example: Stripe payments at 500 orders per minute#

"Ack fast, process async" stays abstract until you size it, so let me put real numbers on it.

  • Checkout peaks at 500 completed payments per minute, roughly 8.3 events per second.
  • Each fulfillment does three things: a database write (10 ms), a Stripe refetch to confirm the amount (120 ms), and an email send (300 ms).
  • That is about 430 ms of real work per event.
Pause before you read on: a flash sale doubles you to 1,000 payments per minute. If you process inline, what breaks first, and what does it cost you?
Reveal the answer
Tail latency crosses Stripe's 20-second timeout. Stripe marks those deliveries failed and retries them with exponential backoff over up to 3 days. The retries land as duplicates while the originals are still running, so one spike becomes a self-inflicted duplicate storm. The fix is not more handlers. It is moving the 430 ms off the request path entirely.

Processing inline.

  • At 8.3 events per second with 430 ms each, you need at least 4 concurrent handlers just to keep up with the mean.
  • Any spike pushes tail latency past Stripe's 20-second timeout.
  • Stripe then retries the slow ones with backoff over up to 3 days.
  • Those retries land as duplicates while the originals are still running: one spike, a duplicate storm.

Ack fast and queue.

  • The receiver verifies the HMAC (about 0.2 ms) and writes one dedup row (about 2 ms), then enqueues and returns 200.
  • Total receiver time is roughly 3 ms, so a single modest instance handles 8.3 events per second with 99.9% headroom.
  • The 430 ms of real work moves to workers draining the queue.
  • Steady state needs ceil(8.3 x 0.43) = 4 workers, and you can scale to 8 during the flash sale without touching the receiver.
  • The queue absorbs the spike, Stripe never sees a slow response, so it never retries.
~3 ms
Receiver time per eventVerify plus one dedup write, versus 430 ms of real work moved to the queue. That gap is the whole design: it is the difference between 4 instances and a retry cascade.

Sizing the dedup store.

  • 500 events per minute with a 24-hour retention window is 720,000 keys.
  • At roughly 100 bytes per key (event ID plus a timestamp), that is about 72 MB in Redis. Trivial.
  • TTL of 24 hours: Stripe's retry window is 3 days, but duplicates in practice cluster within minutes, and a backstop idempotent write covers the rest.

4Trade-offs#

The headline call is webhooks versus polling. Inside "use webhooks" there are more.

Dimension Webhooks Polling
Latency Near real time, event to delivery in ms Bounded by poll interval
Efficiency Traffic only on real events Mostly empty responses
Consumer complexity Must run a public endpoint Just a client on a timer
Reliability model Provider retries, you dedupe Consumer controls its own fetch
Failure visibility Silent if endpoint is down You notice when your poller errors
Firewall friendliness Needs inbound access Outbound only, easy

What you give up with webhooks: control and simplicity.

  • You now operate a public HTTP endpoint that must be up, fast, authenticated, and idempotent.
  • You are at the mercy of the provider's retry policy.
  • If your endpoint is down during a burst, you depend entirely on the provider replaying them, and not every provider does that well.

What you give up with polling: freshness and money.

  • You either accept lag, or you poll aggressively and burn compute on both sides.

A third option worth naming: long polling, server-sent events, or WebSockets.

  • These keep a connection open so the server can push to a client that cannot expose a public URL (a browser, a mobile app).
  • Webhooks are the right tool for server-to-server push where the consumer owns a stable, reachable endpoint.
  • If the consumer is a phone behind a NAT, webhooks do not fit. Reach for a persistent connection or a push notification service.

One trade-off inside delivery semantics: webhooks are at-least-once by nature.

  • Providers retry on failure, and retries plus network uncertainty mean you will receive the same event twice eventually.
  • You cannot get exactly-once over HTTP.
  • You get at-least-once delivery plus idempotent processing, and together those approximate exactly-once effects.
  • Anyone who claims exactly-once webhook delivery is hand-waving.

5Where it is used#

Webhooks are everywhere once you look.

  • Payments. Stripe, PayPal, and friends send payment.succeeded, subscription.cancelled, charge.refunded. The webhook is the source of truth for fulfillment, not the browser redirect, because the user can close the tab before the redirect fires.
  • Version control and CI. GitHub POSTs to your CI system on every push and pull request, so the pipeline starts a build without polling the repo.
  • Chat and collaboration. Slack and Discord deliver message events and slash-command invocations to your bot's endpoint.
  • Communications. Email and SMS providers fire delivered, bounced, opened so you can update contact status.
  • E-commerce. Order platforms notify your inventory system when an order is placed or shipped.

When I reach for a webhook.

  • The event is rare relative to how often I would otherwise poll.
  • Latency matters.
  • The consumer is a server I control with a stable URL.
  • If I am reacting to things in another system, webhooks are the default.
  • If I am observing my own internal services, I lean toward a message queue or event bus instead: inside my own walls I do not need HTTP callbacks, I have Kafka or SQS.

6Common pitfalls and gotchas#

These are the things that actually break in production.

  • No idempotency. The classic bug. A retry delivers payment.succeeded twice and you fulfill the order twice. Fix: every event carries a unique ID (Stripe calls it evt_...). Store processed IDs in a dedup table or a Redis set with a TTL, and make the "mark processed" write atomic with the work itself. Check "have I seen this ID?" before doing anything.
  • Processing inline instead of acking fast. The most common design miss I see in interviews. Verify, enqueue, return 2xx in under a second. Do the work in a worker.
  • Skipping signature verification. Your URL is public. Anyone who learns it can POST fake payment.succeeded events and get free product. Providers sign each payload with a shared secret (HMAC-SHA256 over the raw body). Recompute and compare before trusting anything. Verify against the raw bytes, not a re-serialized object, because JSON key reordering changes the hash.
  • Replay attacks. An attacker captures a valid signed request and sends it again. The signature alone does not stop this, because it is a genuine signature. Include a timestamp in the signed payload and reject anything older than, say, five minutes, combined with your idempotency check.
  • No retry or dead-letter strategy on your side. The provider retries delivery, but once you accept and enqueue, you own reliability. If the worker fails, you need retries with backoff and a dead-letter queue, or events quietly vanish.
  • Ordering assumptions. Webhooks are not ordered. You might get subscription.updated before subscription.created from retries and parallel delivery. Do not assume order. Use the event timestamp or refetch current state from the provider's API when order matters.
  • Silent death. An endpoint returning 500 for an hour, or an expired cert, can stop deliveries with no alarm. Monitor delivery success and alert on a drop. Many providers offer a replay or event-list API: know whether yours does before you need it.
Red flag that fails you here

Treating the webhook as a plain request/response call and assuming the happy path always holds. If you do not say "idempotency key" unprompted when asked about duplicates, the interviewer hears someone who read a blog post but never operated one of these, and they will not trust you with the payment path.

7How it shows up in interviews#

Webhooks usually appear as a piece of a larger design: "design a payment system," "design a CI/CD pipeline," "design a notification service." The interviewer is checking whether you understand event-driven communication and its failure modes.

From the interviewer's chair

I am listening for a named failure behind every happy-path step. Fake events, replays, duplicates, slow responses, worker crashes, reordering, silent death. Give me a happy-path narration with no failure modes and I have quietly downgraded you. Name the receiver-worker split and tell me where the millisecond ack goes, and I know you have run one of these.

The weak answer.

  • "The provider calls my API when something happens and I update my database."
  • Why it fails: no retries, no duplicates, no verification, no async split. It treats the webhook as request/response and assumes the happy path.

The strong answer.

  • Register a signed webhook. On receipt, verify the HMAC-SHA256 against the raw body and reject timestamps older than five minutes.
  • Check the event ID against a Redis dedup store, enqueue, and return 200 in a few milliseconds.
  • A pool of workers processes it idempotently with exponential backoff retries and a dead-letter queue.
  • Do not assume ordering: for state-sensitive events like subscription changes, refetch from the provider.
  • Monitor delivery rate so a silent outage pages me.
  • Why it wins: a concrete failure for every happy-path step, a specific mitigation for each, and a clear picture of where the time budget goes.
Follow-ups the interviewer will ask (and your answers)
"Your provider guarantees at-least-once. How do you get exactly-once effects?"
You do not get exactly-once delivery. You get at-least-once delivery plus idempotent processing: every event carries a unique ID, I check it against a dedup store before acting, and the "mark processed" write is atomic with the work. Retries then become no-ops, so the effect is exactly-once even though delivery is not.
"Why webhooks over polling here, in numbers?"
If 50,000 clients poll every 10 seconds for an hourly event, that is 5,000 requests per second and 99.99% return nothing. Webhooks send zero traffic until an event fires, then one POST, and the latency drops from "up to the poll interval" to milliseconds. The cost is that I now operate a public endpoint that must be up, fast, verified, and idempotent.
"How do you keep the endpoint fast enough that the provider never retries?"
Ack fast, process async. The receiver only verifies the HMAC (about 0.2 ms) and writes one dedup row (about 2 ms), then enqueues and returns 200, roughly 3 ms total. The real 430 ms of work moves to workers draining the queue, which scale independently. The provider never sees a slow response, so it never triggers a retry.
"Someone replays a valid signed request. Does your signature check stop it?"
No, because it is a genuine signature. I include a timestamp in the signed payload and reject anything older than about five minutes, combined with the idempotency check so even a within-window replay is a no-op. Signature proves authenticity; timestamp plus dedup proves freshness and uniqueness.
"You receive subscription.updated before subscription.created. Now what?"
Webhooks are not ordered, so I never assume arrival order. For state-sensitive events I either sort by the event timestamp or, better, refetch the current state from the provider's API when I process it. The webhook becomes a trigger to go read the truth, not the truth itself.

What you can now explain in the room

  • Why a webhook is push on an event and when it beats polling, with the traffic numbers.
  • Why you return 2xx before doing the work, and what a provider timeout costs you.
  • How to size the receiver, workers, and dedup store from a real event rate.
  • Why you can only get exactly-once effects, never exactly-once delivery, over HTTP.
  • How to secure the endpoint: HMAC over raw bytes plus timestamp-based replay protection.
  • Who owns reliability after the ack, and why a silent 500 needs monitoring and a dead-letter queue.

Next, try the System Design Question walkthrough on designing a payment processing system, where webhook reliability is the whole game.

Get a prep plan built around your Webhooks interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Webhooks prep plan is on the way.