How to Prevent Webhook Traffic Spikes from Crashing Your API
How to Prevent Webhook Traffic Spikes from Crashing Your API Rate Limiting, Throttling, and What Actually Happens in Production (2026) Modern systems talk to each other in real...

How to Prevent Webhook Traffic Spikes from Crashing Your API
Rate Limiting, Throttling, and What Actually Happens in Production (2026)
Modern systems talk to each other in real time through webhooks instead of polling. That's great for latency, but it comes with a downside: when a large platform recovers from an outage and flushes its backlog, or a flash sale triggers a wave of transactional events, your endpoint can receive thousands of concurrent POST requests with no warning.
Without safeguards, that influx behaves like an accidental denial-of-service attack on your own database. Threads exhaust, connections queue up, response times climb past the sender's timeout, and you start returning 429 or 502 errors — which, depending on the provider, can make the problem worse rather than better.
This guide covers why these spikes happen, how the major providers actually behave when your endpoint slows down (the real behavior differs a lot by provider, and some documentation is outdated), the core traffic-shaping algorithms you can implement yourself, and the managed options available if you'd rather not build it from scratch.
The Anatomy of a Webhook Spike
A handful of patterns account for most spike-related incidents:
- The backlog flush. A provider has internal downtime, queues up outbound events, and delivers hours of accumulated traffic in seconds once it recovers.
- Bulk data operations. A user runs a script that updates 10,000 records in your GitHub App or Shopify store, and the provider fires one webhook per record, all at once.
- Retry storms. If your endpoint slows down, fresh events arrive at the same time as retries for earlier failures, multiplying load until the system falls over.
- Flash sales and high-traffic events. Legitimate transactional spikes — checkout events, inventory updates — arrive in a short window.
How dangerous the "retry storm" scenario is depends heavily on which provider you're integrating with, and this is where a lot of older advice is wrong.
How Major Providers Actually Retry (Don't Assume)
It's common to see this guidance: "if your endpoint times out, the provider retries with exponential backoff." That's true for some providers and flatly false for at least one major one.
- Stripe retries failed webhook deliveries for up to three days in live mode, using exponential backoff, and disables the endpoint (with an email notice) if failures continue for that whole window. Test-mode retries are much shorter — around three attempts over a few hours.
- Shopify changed its policy in September 2024: it now retries a failed webhook up to eight times over a four-hour window, using exponential backoff, down from a previous ~19 attempts over 48 hours. Shopify treats anything slower than five seconds as a failure. If your endpoint keeps failing, Shopify can automatically delete the subscription outright — new events simply stop arriving.
- GitHub does not automatically retry failed webhook deliveries at all. If your endpoint is down when GitHub tries to deliver an event, that delivery just fails. Recovery is manual: you either redeliver individual events from the Deliveries UI/API within a rolling three-day retention window, or build your own scheduled job that polls for failures and redelivers them.
The practical takeaway: your retry-storm risk from Stripe and Shopify is real and provider-driven. Your risk from GitHub is different — a missed delivery during a spike doesn't come back on its own unless you build that recovery path yourself. Always check a given provider's current webhook docs rather than assuming a universal retry contract; "webhook" as a term implies no particular delivery guarantee.
Why a Standard Synchronous Endpoint Falls Over
Most webhook receivers start out as an ordinary REST endpoint: receive the POST, verify the signature, parse the payload, query the database for an idempotency check, write the record, return 200 OK. That works fine under normal load.
Under spike conditions it fails predictably. Each request holds a thread and a database connection open for the duration of the handler. When a few thousand requests land at once, the connection pool exhausts, queries queue up behind lock contention, and response times climb. Once you cross the provider's timeout (often 5–15 seconds), it marks the delivery as failed and — depending on the provider — schedules a retry, which piles more load onto a system that's already struggling. Eventually you either hit the OOM killer or your load balancer starts returning 502s.
The fix is architectural: decouple ingestion (accepting the request) from processing (doing the actual work), and shape the traffic between the two.
Rate Limiting vs. Throttling
These terms get used interchangeably but describe different strategies:
- Rate limiting rejects requests once a threshold is exceeded, usually with a
429 Too Many Requests. It protects your infrastructure but drops the excess outright. - Throttling (traffic shaping) queues or delays excess requests instead of rejecting them, processing them at a controlled pace.
For third-party webhooks specifically, outright rejection is risky: a 429 just tells a well-behaved sender to retry later, and if the underlying spike is still in progress, those retries compound on top of new traffic. The safer default for inbound webhooks is: accept everything immediately, then throttle the processing.
The Token Bucket Algorithm
A bucket holds a maximum number of tokens (its capacity). Tokens refill at a fixed rate (e.g., 10/second). Each incoming request consumes one token; if the bucket is empty, the request is delayed or rejected.
This is good at absorbing bursts — if the bucket is full, a sudden spike of several hundred requests can pass through immediately — but it strictly enforces the average rate once the bucket empties. A commonly cited rule of thumb from production implementations: set your bucket capacity to roughly 5x your refill rate, so you absorb short spikes without letting a truly massive burst straight through to your database.
The tradeoff: token bucket protects your long-term throughput, but the initial burst still hits your downstream systems all at once — which can be exactly what causes a database to lock up if your capacity is set too generously.
The Leaky Bucket Algorithm
If token bucket allows bursts, leaky bucket eliminates them. Incoming requests go into a queue; they "leak" out the bottom at a strictly constant rate regardless of how fast they arrived. If the queue overflows, excess requests are rejected.
This is the stronger guarantee for protecting a database: even if a provider sends 10,000 events in one second, your database only ever sees a steady, predictable drip. The cost is engineering complexity — a durable leaky bucket needs a real message broker (Kafka, RabbitMQ, SQS, Redis Streams) and separately scaled worker processes to drain it.
A few refinements that production systems actually use on top of the basic leaky bucket:
- Per-destination rate limits. If you're delivering to many downstream consumers (a multi-tenant product, for instance), each destination gets its own bucket, since they can handle different volumes.
- Per-account fairness caps. One account with 10,000 backlogged events shouldn't starve delivery capacity for every other account. A simple concurrency cap per account (e.g., no more than 10 in-flight deliveries) prevents this.
- Priority ordering. Not all events matter equally — a payment confirmation should usually be processed ahead of a routine profile update. Adding a priority field to the queue and sorting by it before FIFO order handles this cheaply.
- Respect
Retry-After. When a downstream system responds with429or503and includes aRetry-Afterheader (an RFC 9110-defined header, given as either seconds or an HTTP date), honor it directly rather than layering your own backoff calculation on top — it's a more authoritative signal than anything you'd compute yourself.
Circuit Breakers for Webhook Ingress
Rate limiting and throttling control volume, but they don't help if the downstream system itself is unhealthy — a database under maintenance, a dependency that's down. Continuing to accept and queue work against a failing backend just delays the failure and wastes resources. The circuit breaker pattern, borrowed from electrical engineering, addresses this directly and has three states:
- Closed (normal). Traffic flows normally while the breaker monitors success/failure rates.
- Open (failing). Once failures cross a threshold (e.g., 20% error rate over 10 seconds), the breaker trips. It stops sending traffic downstream and fails fast — returning
503immediately rather than letting requests time out slowly. This gives the backend room to recover. - Half-open (testing). After a cooldown period, a limited number of test requests are allowed through. If they succeed, the breaker closes again; if they fail, it reopens.
Placed at the ingress layer, a circuit breaker means a bulk-operation flush from a provider doesn't finish off an already-degraded database. The provider gets fast failures and applies its own backoff schedule on its end.
The Architecture That Actually Holds Up
Putting this together, the pattern most resilient webhook receivers converge on in 2026 looks like this:
- Ingress layer. A lightweight, horizontally scalable endpoint (often serverless or behind an API gateway) receives the request.
- Validate. Verify the HMAC signature quickly — this is cheap and should happen before anything else.
- Enqueue. Push the raw payload onto a durable queue (Kafka, SQS, Redis Streams) without doing any business logic.
- Acknowledge immediately. Return
200/202right away — ideally in well under a second — so the provider never has reason to consider the delivery failed. - Worker layer. A separately scaled pool of workers pulls from the queue at a controlled, rate-limited pace and does the real work: idempotency checks, database writes, downstream API calls.
On the receiving side, idempotency is not optional. Every provider's retry mechanism implies at-least-once delivery, so your handler will see duplicate events — sometimes from retries, sometimes from your own worker timing out and re-processing before the first attempt's write commits. Dedupe on the provider's event ID (Stripe's event.id, GitHub's X-GitHub-Delivery, Shopify's X-Shopify-Webhook-Id) before doing any side-effecting work, and keep that dedupe record around at least as long as the provider's retry window.
This architecture is genuinely effective, but it's real infrastructure to build and operate: provisioning a broker, autoscaling workers against queue depth, and implementing dead-letter handling for events that exhaust their retries.
Managed Options, If You'd Rather Not Build It
Several vendors offer this "accept-then-throttle" layer as a managed service, so you point your provider at their URL instead of your own infrastructure. It's worth comparing a few rather than assuming any single one covers everything:
- Hookdeck documents a per-destination token-bucket rate limiter (built on Redis, with worker pools that autoscale against queue depth), configurable retry schedules, and an "Event Gateway" model that acknowledges receipt immediately and separates ingestion from delivery.
- Svix and Convoy are widely used for the outbound side of the problem — reliable webhook delivery to your customers, with configurable retry schedules and delivery logs. Convoy is open source and self-hostable if you need to keep data in-house.
- InstaWebhook provides durable webhook intake (it stores the event and queues delivery before returning a response), delivery timelines, configurable retry policies with backoff, replay controls, a dead-letter queue for exhausted events, HMAC signing, audit logging, and an option to store payloads in your own PostgreSQL schema rather than the vendor's. As with any vendor, verify current rate-limiting/throughput specifics and pricing directly against their docs before committing, since these details change.
None of these remove the need to understand the underlying algorithms — you'll still need to configure sensible bucket sizes, retry windows, and per-destination limits regardless of who operates the queue.
Conclusion
Webhook traffic is only going to get bursier as more of the API economy becomes event-driven. A synchronous, unshaped endpoint cannot survive a real backlog flush or bulk-operation spike. The fix isn't a single trick — it's a combination of accepting fast, queueing durably, throttling the actual processing at a rate your database can sustain, breaking the circuit when a downstream dependency is unhealthy, and treating every event as a potential duplicate. Whether you build that stack yourself with a message broker and worker pool, or offload it to a managed ingress buffer, the underlying goal is the same: never let the provider's burst become your database's problem.