12 System Design Concepts Every Developer Should Know
When you build software that serves real users, system design means deciding how its parts store data, handle requests, and respond when…
When your service receives a webhook, another service has sent it an HTTP request to report an event, such as a payment update or a code change. Your endpoint needs to accept that request quickly and safely. If it responds too slowly or returns an error, the sender may retry, stop delivering, or require you to recover the missed event yourself.
A reliable receiver verifies each request, then stores the event so it survives a crash before returning a successful HTTP response. It handles slower work in the background and accounts for the same event arriving more than once or events arriving out of order. The exact timeout and retry rules depend on the sender, so check each provider’s policy rather than assuming one standard applies.
A sender generally treats a timely HTTP 2xx response, with a status code from 200 to 299, as successful receipt. A timeout or other response may count as a failed delivery. The deadline and what happens after failure vary by provider:
| Provider | Documented delivery behavior | What to plan for |
|---|---|---|
| Stripe | In live mode, Stripe retries for up to three days with exponential backoff, meaning the delay between attempts increases. Its sandbox behavior is different: three retries over a few hours. Stripe also supports manual resends through the Dashboard for up to 15 days and through the CLI for up to 30 days after event creation. | Handle repeated deliveries and distinguish live from sandbox behavior. A manual resend does not necessarily stop automatic retries. |
| GitHub | GitHub records a delivery as failed if it takes longer than 10 seconds and does not automatically redeliver failed deliveries. | Return a 2xx response within the deadline and arrange your own review and recovery of failures. |
| Shopify | Shopify treats a delivery as failed if the endpoint does not respond within five seconds. The time between retries increases, and Shopify can remove subscriptions after multiple failures. | Watch delivery attempts and response times so repeated failures do not go unnoticed. |
| Svix | Svix describes a 2xx response within 30 seconds as successful for its service. | Use the limit for Svix integrations only. It is not a general webhook timeout. |
These limits come from each provider’s documentation: Stripe’s webhook guidance, GitHub’s failed-delivery guidance, Shopify’s troubleshooting guidance, and Svix’s receiving guide. A service that receives webhooks from several providers should meet the shortest relevant deadline while following each provider’s retry and recovery rules.
Return a successful response as soon as your service has verified the request and safely recorded the event. A durable record is data that survives a process crash, such as a committed database row or a message accepted by a durable queue. Your handler should not return success just because it started processing: if the service stops before recording the event, the sender may treat the delivery as complete while your service loses the event.
In a common design, the service checks the sender’s signature to confirm the request came from that sender and has not changed. It then stores the event or adds it to a queue for later work before returning 2xx. A background worker performs slower tasks, such as calling another API or updating several records. Both Stripe and GitHub recommend moving complex work out of the request path so the endpoint can respond promptly (Stripe, GitHub).
Stripe requires the original, unmodified request body for its signature check. If a request fails verification, check that your code uses the correct secret, signature header, and body representation before changing retry settings.
A timeout does not prove that your handler did nothing. Your service might finish the work while the response gets lost, leaving the sender to retry. The receiver should therefore assume that the same event can arrive more than once. Stripe documents duplicate deliveries and recommends tracking event IDs; it also warns that events can arrive out of order.
Store a stable provider event or delivery ID and use it to prevent the same event from causing irreversible work twice. For example, a payment handler should not issue a second refund just because the sender redelivered an event. Record the event and create its work item in the same database transaction when possible, so a crash cannot leave the event marked as accepted without corresponding work.
Distinct events can also describe the same underlying change, so an event ID alone does not resolve every duplicate-effect problem. Apply domain rules to the action itself, such as checking whether a refund has already been issued. For late or reordered events, check that each event moves the object from a valid current state to a valid next state, or retrieve the provider’s current object before changing your stored state. Do not use event timestamps as a substitute for deduplication or guaranteed ordering.
Start with the provider’s delivery record, then match it against your infrastructure and application logs. A delivery record shows what the sender attempted and what response it observed; it does not show every step inside your network or confirm that background work completed. GitHub also notes that delivery logs can take a few minutes to show an event.
Keep secrets out of logs, and avoid storing more sensitive payload data than debugging requires. Log enough information to distinguish a request that never reached your endpoint from one that your endpoint accepted but a worker failed to process.
Automatic retries have limits, and some senders do not provide them at all. Track accepted events that remain unprocessed and failed deliveries that need review. Give operators a safe way to replay work while preventing the same event from causing the same action twice, as you do for ordinary deliveries.
When an outage creates a gap, use the provider’s manual resend tools or API where available. For events that cannot be replayed, compare your stored state with the provider’s current records and correct any differences. GitHub recommends finding and redelivering failed deliveries because it does not retry automatically; Stripe provides bounded manual resend windows, which do not replace duplicate-safe processing.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com