System design template

Webhook delivery architecture.

You are making requests to servers you do not control, which are frequently slow, sometimes wrong, and occasionally gone.

Download for macOS
v0.1.33 · .dmg · Apple Silicon & Intel
Signed & notarized by Apple · opens without a Gatekeeper warning
sha256 698955a0187bc039f4c74f5d05a9f10fbb27376a45788a0a241d1326b73873c7
Download for Windows instead
$curl -fsSL https://lucidtrain.com/install.sh | sh
Webhook delivery system architecture. 11 components across 4 tiers.
Hover any component to see what it is responsible for.
Webhook delivery system architecture. Rendered by the same ELK layout engine the app runs: the agent emits components, tiers and edges, and the engine places them, so the boxes cannot overlap.

The components

Every row below is read from the graph that produced the diagram above, so the two cannot disagree.

ComponentTierWhy it is there
Producing servicesClientSupporting component
Subscription APIApplicationEndpoints, secrets and event filters
DispatcherApplicationFans one event to every matching subscription
SignerApplicationHMAC with a timestamp, so replays are detectable
Delivery workersApplicationPer-endpoint concurrency, so one slow customer is contained
Circuit breakerApplicationPauses an endpoint that keeps failing
Retry schedulerApplicationExponential backoff with jitter
KafkaDataSupporting component
Dead letter queueDataSupporting component
PostgresDataAttempt history, replayable by the customer
Customer endpointsExternalSupporting component

Design decisions worth arguing about

A diagram shows what was chosen. It does not show what it cost, and that is usually the part that matters in a review or an interview.

Per-endpoint concurrency, or one customer stalls everyone

A shared worker pool means one customer whose endpoint takes thirty seconds occupies workers that everyone else needs, and during an incident on their side you have an incident on yours. Limiting concurrency per endpoint contains it. The cost is more scheduling state and some idle capacity, which is much cheaper than the coupling it removes.

Sign with a timestamp, not just a body hash

An HMAC over the payload proves it came from you and does nothing against replay: a captured request can be sent again indefinitely. Including a timestamp in the signed material and requiring recipients to reject old ones closes that. It means recipients need roughly correct clocks, which is a support burden you inherit.

Circuit breaking is politeness and self-protection

An endpoint returning errors for an hour will keep doing so, and continuing to retry wastes your workers and hammers someone whose system is already unwell. Pausing that subscription after a threshold and probing occasionally is better for both sides. The risk is pausing on a transient failure, so the threshold has to be forgiving enough not to fire on a blip.

At-least-once, and say so loudly

Guaranteeing exactly-once delivery to an endpoint you do not control is not achievable: a timeout is indistinguishable from a slow success. So delivery is at-least-once, every payload carries a stable event id, and the documentation tells consumers to deduplicate. Pretending otherwise pushes an unsolvable problem onto customers without warning them.

The attempt log is the support tool

Most webhook support tickets are a customer asking whether you sent something. Storing every attempt with its response code and body, visible to the customer, and letting them replay from it, resolves that class of ticket without a human. It costs real storage on high-volume accounts, and it is worth it.

How it changes with scale

Volume is events times matching subscriptions, so a popular event type with many subscribers is the multiplier that surprises people. Throughput is bounded by the slowest endpoints rather than by your own capacity, which makes per-endpoint isolation the thing that determines whether the system degrades gracefully.

Where it breaks first

A slow endpoint rather than a failing one. A dead endpoint fails fast and the breaker opens; one that takes twenty-nine seconds against a thirty-second timeout consumes a worker for the entire window while looking healthy. Without per-endpoint limits and an aggressive timeout, a handful of slow consumers can consume the whole pool while every dashboard says everything is fine.

Draw this yourself

Open the Diagram tab and describe the system. The agent emits a semantic graph rather than coordinates, so you can edit the components and the layout re-solves instead of drifting.

shell
$ Diagram a webhook delivery system: subscription API, dispatcher fanning events to subscriptions, HMAC signing, delivery workers with per-endpoint concurrency, circuit breaker, retry scheduler with backoff, dead letter queue and a replayable attempt log.

When the shape is right, Implement in code turns the canvas into a markdown specification, every component, every relationship and the notes, and starts a real turn in the Code tab with it.

FAQ

Questions about this design

How long should retries continue?

Long enough to survive a deploy on the receiving side, typically hours with exponential backoff and jitter, then to the dead letter queue. Retrying for days delivers events whose usefulness expired long before.

How should the receiver verify a webhook?

Recompute the HMAC over the raw body with the shared secret and compare in constant time, then check the timestamp is recent. Parsing the body before verifying is a common and avoidable mistake.

Should webhooks be ordered?

Avoid promising it. Ordering across retries and concurrent deliveries is expensive and constrains throughput. Send a sequence number and let consumers order if they care.

What response should the receiver return?

A 2xx as soon as the event is durably queued on their side, not after processing it. Receivers that do the work synchronously before responding are the main source of timeouts.

More templates