Deep Dive - Webhooks, Events & Reliability
High-throughput delivery with retries, DLQs and idempotency
Delivery guarantees & semantics
After this you can reason precisely about what "reliable delivery" means.
"Reliable" is not a single number. It is a choice between three failure modes and an engineering manager who owns webhooks for a 1M+ DAU tool has to name the choice out loud before designing anything.
The Cursor JD calls out webhook infrastructure with "proper retry semantics, dead-letter queues, idempotency and observability" in almost those words. When you answer a question in the loop, anchor to that vocabulary on purpose. It shows you read the charter and that you think in delivery guarantees, not vibes.
Start with the three semantics every event system picks from. Each one moves the pain to a different place.
- Semantic
- At-most-once
- What can go wrong
- Events can silently drop; no retry
- Where it fits
- Fire-and-forget metrics, low-value telemetry
- Semantic
- At-least-once
- What can go wrong
- Events can arrive more than once; consumer must dedup
- Where it fits
- The sane default for webhooks and most event delivery
- Semantic
- Exactly-once
- What can go wrong
- Impossible end to end in a distributed system; only approximated
- Where it fits
- A marketing claim unless you read the fine print
| Semantic | What can go wrong | Where it fits |
|---|---|---|
| At-most-once | Events can silently drop; no retry | Fire-and-forget metrics, low-value telemetry |
| At-least-once | Events can arrive more than once; consumer must dedup | The sane default for webhooks and most event delivery |
| Exactly-once | Impossible end to end in a distributed system; only approximated | A marketing claim unless you read the fine print |
Exactly-once delivery is not real across a network. Exactly-once processing is real and you get it by pairing at-least-once delivery with an idempotent receiver.
The sender cannot tell the difference between "the receiver never got it" and "the receiver got it but the ack was lost." Faced with that ambiguity it must either resend (risking a duplicate) or not (risking a loss). There is no third option over an unreliable network. So you choose at-least-once and push correctness onto the consumer, which is the one place you can actually enforce it.
The answer that wins the roomsay it in one breath
When an interviewer asks how you guarantee delivery, lead with the default and the consequence in the same sentence: at-least-once delivery plus idempotent consumers. That single clause tells them you know exactly-once is fiction and that you have already decided who owns dedup.
"I default to at-least-once delivery, then make every consumer idempotent so a duplicate is a no-op. That gives me exactly-once processing without pretending I have exactly-once delivery, which doesn't exist across a network."
Ordering: expensive and usually not what you needglobal vs per-key vs none
Strict global ordering forces serialization, which kills throughput and creates a single point of contention. Most webhook problems only need ordering within a key, like a single repository or a single tenant. Be ready to size the requirement instead of granting it by reflex.
- No ordering
- Cheapest, fully parallel. Fine when each event is self-contained (e.g. "build finished").
- Per-key ordering
- Order within a partition key (repo, tenant, resource). Scales horizontally; the usual right answer.
- Global ordering
- One serialized stream. Reach for it only when a downstream genuinely needs total order and pay for it knowingly.
"created then deleted" arriving out of order matters; two unrelated push events do not. Order per resource, not globally.
Inbound vs outbound: two different reliability problemsthe distinction senior candidates draw and juniors miss
Cursor sits on both sides of the webhook contract. It ingests events from GitHub, GitLab and Bitbucket and it may emit events to customers and internal surfaces. The reliability concern flips depending on which direction you're standing in.
You don't control the sender or its retry policy.
Ack fast (return 2xx in milliseconds), then process async - or the provider times out and disables your endpoint.
Verify the signature before you trust a byte.
Assume duplicates and out-of-order delivery from day one.
You own the retry policy, backoff and DLQ.
Customers' endpoints will be flaky; design for their 500s, not just yours.
Sign your payloads so they can verify you.
Your idempotency story is now a published contract.
GitHub and most providers will auto-disable a webhook that returns slow or non-2xx responses repeatedly. If your handler does the real work inline before acking, one slow database call can get your integration silently turned off across every customer. Ack first, queue, then process.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Same word, two opposite jobs - name which side you're on before you design.
Takeaway. Default to at-least-once delivery plus idempotent consumers (that's how you get exactly-once processing); pick the weakest ordering that's still correct (usually per-key); and treat inbound SCM webhooks and outbound customer webhooks as two distinct reliability problems.
Self-check
QAn interviewer asks: "Can you guarantee exactly-once webhook delivery?" What's the strongest answer?
Retries, backoff & DLQs
After this you can design a retry pipeline that survives transient and permanent failures.
At-least-once delivery is a promise you keep with a retry loop. The whole craft is retrying enough to ride out a blip without turning a downstream hiccup into a self-inflicted outage.
The naive version - retry immediately, forever - is how you take down a recovering service. Ten thousand clients all retry at the same instant the endpoint comes back and you knock it over again. That's a thundering herd and the fix is two ideas working together.
- 1Exponential backoff. Wait longer after each failure: 1s, 2s, 4s, 8s, 16s. Spacing retries out gives a struggling endpoint room to breathe instead of a flood.
- 2Jitter. Multiply each computed delay by a random factor (commonly 0–1, i.e.
delay * random()). This smears retries across time so synchronized clients stop hammering in lockstep. - 3A retry cap. After N attempts (say 5–8 over tens of minutes to a few hours), stop. Infinite retries clog the pipeline and waste compute on something that isn't coming back.
- 4Route to a dead-letter queue. When the cap is hit, move the event to a DLQ for isolation, alerting and later replay - don't drop it.
base = 1.0 # seconds
cap = 300.0 # max 5 min between attempts
def delay(attempt):
# exponential growth, then full jitter
expo = min(cap, base * (2 ** attempt))
return random.uniform(0, expo)
# attempt 0 -> up to 1s, attempt 3 -> up to 8s, attempt 7 -> up to ~128sPlain exponential backoff still synchronizes: every client that failed at the same moment retries at the same moment, just later. Jitter is what actually breaks the lockstep. "Full jitter" (uniform between 0 and the computed delay) is the AWS-blessed default and is the right thing to name in an interview.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
An event's whole journey - and the gate where a stuck event leaves the hot path.
Retryable vs non-retryable: stop hammering a 400the classification that saves your error budget
Not every failure deserves a retry. A 500 or a timeout might succeed next time. A 400 or a 401 will fail identically on every attempt, so retrying just burns capacity and delays the moment you notice the real problem.
- Failure
- 5xx server error
- Retry?
- Yes, with backoff
- Reasoning
- Likely transient - overload, deploy, dependency blip
- Failure
- Timeout / connection reset
- Retry?
- Yes, with backoff
- Reasoning
- Network or slowness; the request may simply not have landed
- Failure
- 429 Too Many Requests
- Retry?
- Yes, but honor Retry-After
- Reasoning
- Explicit backpressure; respect the header instead of guessing
- Failure
- 4xx (400, 401, 404, 422)
- Retry?
- No - straight to DLQ
- Reasoning
- Bad request, bad auth or gone; retrying changes nothing
| Failure | Retry? | Reasoning |
|---|---|---|
| 5xx server error | Yes, with backoff | Likely transient - overload, deploy, dependency blip |
| Timeout / connection reset | Yes, with backoff | Network or slowness; the request may simply not have landed |
| 429 Too Many Requests | Yes, but honor Retry-After | Explicit backpressure; respect the header instead of guessing |
| 4xx (400, 401, 404, 422) | No - straight to DLQ | Bad request, bad auth or gone; retrying changes nothing |
Hammering a permanently-broken endpoint wastes your throughput and hides the signal that something needs human attention.
The DLQ is a debugging and recovery tool, not a graveyardwhat it buys an EM running on-call
A dead-letter queue isolates the events your pipeline couldn't deliver so they don't block everything behind them. The events sit there, inspectable, while the main flow keeps moving. That separation is what lets you fix a root cause calmly and then replay.
Poison messages leave the hot path.
One broken consumer can't stall the queue for everyone.
DLQ depth is a leading incident signal.
Each message carries why it failed - fast triage.
Fix the bug, then re-drive the DLQ.
Idempotent consumers make replay safe to repeat.
When you sketch the retry path, alert on two signals out loud: DLQ depth climbing and retry rate spiking. Both are leading indicators - they move before customer-facing success rate craters. Saying "these page me before the SLO breaks" reframes you from someone who builds pipelines to someone who operates them, which is the whole point of an EM round.
A DLQ with no alarm and no replay tooling is just a slow data leak. If the answer to "what happens after the DLQ?" is "we look at it eventually," you've described losing events with extra steps. Always pair the DLQ with an alert on its depth and a documented replay path.
Takeaway. Retry transient failures with exponential backoff + jitter, cap the attempts and route exhausted or non-retryable (4xx) events to a DLQ that you alert on and can replay - and never inline-process before acking the sender.
Self-check
Idempotency & dedup
After this you can make repeated delivery safe end to end.
At-least-once delivery means duplicates are not an edge case - they are a guarantee. The system's correctness rests entirely on the receiver treating the second copy of an event as a no-op.
Idempotency is the contract that makes this safe. Each event carries a stable key, the consumer records the keys it has already handled and any repeat is recognized and skipped. Without it, a retry that succeeds after a lost ack double-charges, double-provisions or double-fires a downstream action.
Where the key livesfast, durable and TTL'd
- Store
- Redis SET NX with TTL
- Strengths
- Microsecond checks, automatic expiry
- When to reach for it
- High-throughput hot path; a bounded dedup window is acceptable
- Store
- Unique-constrained DB column
- Strengths
- Strong durability, transactional with the side effect
- When to reach for it
- When dedup and the business write must be atomic
- Store
- Both (Redis as cache, DB as truth)
- Strengths
- Speed plus a durable backstop
- When to reach for it
- Large scale where you want fast rejects but can't lose the record
| Store | Strengths | When to reach for it |
|---|---|---|
| Redis SET NX with TTL | Microsecond checks, automatic expiry | High-throughput hot path; a bounded dedup window is acceptable |
| Unique-constrained DB column | Strong durability, transactional with the side effect | When dedup and the business write must be atomic |
| Both (Redis as cache, DB as truth) | Speed plus a durable backstop | Large scale where you want fast rejects but can't lose the record |
Whatever the store, the check and the side effect should be effectively atomic - otherwise two concurrent duplicates both pass the check.
# SET key value NX EX ttl -> returns OK only if the key did not exist
ok = redis.set(f"evt:{event_id}", "1", nx=True, ex=86400)
if not ok:
return # already processed within the 24h window: no-op
process(event) # safe: we hold the claimCheck-then-set in two steps is a race: two duplicates arriving together both read "not seen" and both proceed. Use an atomic primitive - Redis SET NX or a unique constraint that throws on the second insert. The atomicity is the whole point; a non-atomic dedup check is a dedup check that doesn't work under load.
Signing and verification: trust before you dedupHMAC + timestamp
Before a consumer dedups an event it should know the event is genuine. Providers sign the payload with a shared secret using HMAC and the receiver recomputes the signature to verify it. Include a timestamp in the signed material and reject anything too old or an attacker can capture a valid request and replay it later.
- 1Sender computes
HMAC-SHA256(secret, timestamp + "." + body)and sends it in a header. - 2Receiver recomputes the same HMAC over the raw body it received and compares with a constant-time equality check.
- 3Reject stale timestamps outside a small window (e.g. ±5 minutes) so a captured-and-replayed payload fails even though its signature is valid.
- 4Then verify the signature, then dedup by event id, then process.
When Cursor emits webhooks to customers, the idempotency key you put in the payload is part of the service contract. Customers build their own dedup against it. If you change or drop that key, you break consumers downstream. Treat it like any other API surface: versioned, documented and stable. This is exactly the "clean abstractions and well-defined service contracts" the JD asks an EM to own.
The trade-off you'll be pushed onwindow vs cost
- Longer TTL
- Catches duplicates that arrive far apart (a DLQ replay days later). Costs more storage.
- Shorter TTL
- Cheap, but a late duplicate slips through after the key expires.
- How to size it
- TTL ≥ your maximum retry-plus-DLQ-replay horizon. If you can replay a DLQ after 7 days, a 1-hour dedup window is a bug.
The dedup window must outlive the longest path a duplicate can take to reach the consumer.
Takeaway. Make consumers idempotent with an atomic claim (Redis SET NX or a unique constraint) keyed on a stable event id, verify HMAC + timestamp before trusting a payload and size the dedup TTL to outlive your longest retry/DLQ-replay horizon.
Self-check
QYour dedup TTL is 1 hour, but operators can replay the DLQ up to 7 days after an incident. What goes wrong and what's the fix?
Throughput, queues & backpressure
After this you can scale event delivery under bursty 1M+ DAU load.
Agent workflows are spiky. A cohort of developers kicks off background agents within the same few minutes of a workday and the event rate jumps an order of magnitude, then falls. The system has to absorb that without dropping events or starving the rest of Core Services.
The first move is to decouple. A queue between producers and consumers turns a spike into a backlog the consumers drain at their own pace, instead of a flood that overruns them. The receiver acks fast, enqueues and returns; workers pull and process. The queue is the shock absorber.
- Decoupling
- Producers don't wait on consumers; a slow consumer doesn't slow ingestion.
- Burst absorption
- A 10x spike becomes a temporary backlog, not dropped events or 503s.
- Durability
- Events survive a consumer crash or deploy because the broker holds them.
- Independent scaling
- Add workers to drain faster without touching the ingest path.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Each layer absorbs a different kind of failure so the one below it stays calm.
Fan-out and per-consumer limitsstop one slow endpoint from taking everyone down
One event often has many destinations - an SCM push might notify an indexer, a build trigger and an audit log. Fan-out duplicates the event onto separate per-consumer queues so each subscriber moves at its own speed. The reason this matters is failure isolation.
One slow consumer backs up the head of the line.
A failing endpoint's retries starve healthy ones.
Head-of-line blocking turns one bad subscriber into a global incident.
Each subscriber has its own backlog and its own pace.
A cap on in-flight deliveries per endpoint contains a slow one.
A broken endpoint fills only its own queue and DLQ.
Backpressure and partitioningshed load deliberately; isolate noisy neighbors
When consumers can't keep up, the system must push back rather than melt. Backpressure means the queue depth and rate limits signal producers to slow down or shed low-priority work, so a flood of events doesn't cascade into auth, agent backend and the rest of the shared layer. Partitioning by tenant or key gives you horizontal scale and keeps one heavy customer from drowning everyone else.
- Lever
- Per-consumer concurrency cap
- What it protects against
- A slow endpoint monopolizing workers
- Trade-off
- Caps max throughput to that endpoint
- Lever
- Backpressure / load shedding
- What it protects against
- Cascading overload into the rest of Core Services
- Trade-off
- Some low-priority events delayed or dropped on purpose
- Lever
- Partition by tenant/key
- What it protects against
- Noisy-neighbor starvation; uneven load
- Trade-off
- Hot partitions still need rebalancing
- Lever
- Rate limit per source
- What it protects against
- A runaway producer flooding the pipeline
- Trade-off
- A legitimate burst may get throttled
| Lever | What it protects against | Trade-off |
|---|---|---|
| Per-consumer concurrency cap | A slow endpoint monopolizing workers | Caps max throughput to that endpoint |
| Backpressure / load shedding | Cascading overload into the rest of Core Services | Some low-priority events delayed or dropped on purpose |
| Partition by tenant/key | Noisy-neighbor starvation; uneven load | Hot partitions still need rebalancing |
| Rate limit per source | A runaway producer flooding the pipeline | A legitimate burst may get throttled |
Each lever trades raw throughput for isolation. At 1M+ DAU, isolation is usually the better buy.
Capacity planning: have the numbers readywhat an EM is expected to quantify
An interviewer will probe whether you operate from data or hand-waving. Come with the three numbers that define this system's capacity and know where the bottleneck moves when load grows.
- Peak event rate - events/sec at the spikiest minute of the day, not the daily average. Size for the spike.
- p99 delivery latency target - the SLI you commit to; e.g. 99% of events delivered within N seconds of ingestion.
- The current bottleneck - broker throughput, consumer concurrency or the downstream endpoint. Name it, because that's where the next dollar of capacity goes.
When asked to scale the system, resist adding workers first. Say: "Before I scale consumers, I want to know the bottleneck - if it's the downstream endpoint, more workers just deepen the backlog and worsen p99." Diagnosing before throwing capacity at it is exactly the player-coach instinct Cursor screens for and it separates you from candidates who reach for autoscaling as a reflex.
Takeaway. A queue decouples bursty producers from consumers; fan-out with per-consumer concurrency caps isolates slow endpoints; backpressure and tenant partitioning stop one source from cascading into Core Services - and you should arrive knowing peak rate, p99 target and the current bottleneck.
Self-check
QOne customer's webhook endpoint goes slow and starts timing out. With a single shared delivery queue, why does this become everyone's problem - and what design prevents it?
Observability & on-call
After this you can make the system debuggable and operable in production.
The JD lists observability alongside retries and DLQs as a first-class requirement and for an EM it's where reliability stops being architecture and becomes a thing the team manages day to day.
A webhook system fails quietly. Events don't crash a page; they just don't arrive. So the only way you know the system is healthy is the instrumentation you built. Cover the three signals and make each one map to a decision.
- Signal
- Delivery success rate
- What it tells you
- Are events actually landing?
- Acts as
- Primary SLI - the number your SLO is written against
- Signal
- Retry rate
- What it tells you
- Is something downstream getting flaky?
- Acts as
- Leading indicator - moves before success rate drops
- Signal
- DLQ depth
- What it tells you
- How many events have we given up delivering?
- Acts as
- Leading indicator + a work queue for humans
- Signal
- p99 delivery latency
- What it tells you
- Are events timely, not just eventually delivered?
- Acts as
- Second SLI - "delivered" without "on time" hides problems
| Signal | What it tells you | Acts as |
|---|---|---|
| Delivery success rate | Are events actually landing? | Primary SLI - the number your SLO is written against |
| Retry rate | Is something downstream getting flaky? | Leading indicator - moves before success rate drops |
| DLQ depth | How many events have we given up delivering? | Leading indicator + a work queue for humans |
| p99 delivery latency | Are events timely, not just eventually delivered? | Second SLI - "delivered" without "on time" hides problems |
Pair metrics with traces across the pipeline (ingest → queue → worker → endpoint) and structured logs keyed by event id so one event is greppable end to end.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Ranked by how early each signal warns you - page on the leaders, write SLOs against the laggards.
SLOs, SLIs and error budgetsan objective bar the team manages to
An SLI is the measurement (delivery success rate). An SLO is the target you commit to (99.9% of events delivered within 30s). The error budget is what's left over - the 0.1% you're allowed to miss. That budget is a management tool, not just a metric.
- Budget healthy
- Ship faster, take more risk on changes - you have room.
- Budget burning fast
- Freeze risky deploys, redirect the team to reliability work.
- The real value
- It turns "how reliable is enough?" from an argument into a number the whole team agrees to in advance.
Error budgets convert reliability from opinion into a shared, objective bar - which is how you lead a team to it instead of relitigating it every sprint.
Designing the on-call experiencewhat the engineer at 3am actually sees
As the manager, you own whether on-call is humane or miserable. The bar is that a single engineer, woken at 3am, can diagnose and act from the runbook and dashboard alone without paging you. Build toward that.
- 1Alert on leading indicators. Page on DLQ depth and retry-rate spikes, not only on a success-rate crater that customers have already felt.
- 2One runbook per failure mode. "DLQ growing," "provider returning 5xx," "consumer lag rising" - each with the dashboard link, the likely cause and the first action.
- 3Dashboards that answer the first question. The on-call's opening question is "what changed?" - put delivery rate, retry rate, DLQ depth and recent deploys on one screen.
- 4Clear ownership per failure mode. Every alert routes to a team that can act on it; an alert with no owner is noise that trains people to ignore the pager.
After the incident: the management half of reliabilityblameless and it feeds the backlog
Reliability work doesn't end when the page resolves. A blameless post-incident review asks what about the system let this happen, not who typed the wrong command. Then the action items become real backlog with owners and dates, so the same incident doesn't recur. Running that loop well is a management practice and it's a strong thing to show in a behavioral round.
"I run reviews blameless because I want the honest timeline and you don't get honesty when people are protecting themselves. Then every review produces backlog items with owners - an incident that doesn't change the system is an incident you'll have again."
Tie observability explicitly back to the charter. Say: "The JD names observability next to retries and DLQs because in event delivery you can't see failures any other way - so I instrument the pipeline so a duplicate, a stuck consumer and a growing DLQ are all visible before a customer files a ticket." Connecting a design choice to the role's stated ownership reads as someone who's read the job, not just the textbook.
Takeaway. Instrument delivery success, retry rate, DLQ depth and p99 latency; write SLOs with an error budget as the team's objective reliability bar; and design on-call so one engineer can act from runbook + dashboard alone, then close the loop with blameless reviews that become backlog.