All Webhook Errors

Webhook 429 Too Many Requests — Causes & Fixes

A webhook 429 Too Many Requests response means a rate limiter — yours, your reverse proxy's, or your cloud provider's — refused the delivery because too many requests arrived in too short a window. With webhooks this is almost always self-inflicted: either you set a too-tight limit, or your handler is slow enough that retries pile up.

Root Causes

1. Receiver-side rate limit set too tight

You added a rate limiter to your webhook endpoint to defend against forgery, set it at "100 requests per minute," and forgot that legitimate Stripe webhooks burst at 50/sec during peak. Now legitimate deliveries are 429ing.

2. Retry storms after recovery

Your handler was down for an hour. It comes back up. Stripe / GitHub / Shopify all start retrying their queued failed deliveries simultaneously. The recovered service sees 5-10x normal traffic for a few minutes. Without queue ingestion, the limiter kicks in and 429s legitimate retries — extending the outage.

3. Cloudflare / API Gateway free-tier limits

Cloudflare Workers Free plan caps at 100k requests/day. AWS API Gateway has account-level throttles (default 10,000 req/sec, 5,000 burst). Vercel hobby plan has function execution limits. If your webhook traffic is high enough to hit these, you'll see 429 / 503 from the platform.

4. Per-IP rate limits applied to a CDN egress IP

You rate-limit "per IP" but the provider routes through a small set of egress IPs. All your webhook traffic appears to come from 5-10 IPs. Per-IP limits pretend each IP is a different attacker — but it's the same provider. Switch to per-route limits.

Fix It

// Express — generous limit on webhook routes, tight on auth routes
import rateLimit from 'express-rate-limit'

const webhookLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 1000,                  // 1000/min on webhook route
  standardHeaders: true,      // emit Retry-After
  legacyHeaders: false,
  skipFailedRequests: true,   // don't count handler failures toward limit
})

app.post('/webhooks/stripe', webhookLimiter, webhookHandler)

If You Must Return 429, Do It Right

Always include a Retry-After header. Most webhook senders honor it. Stripe respects Retry-After; GitHub respects it; Shopify respects it.

// Set Retry-After when 429ing
res.status(429)
   .set('Retry-After', '60')      // seconds
   .send('rate-limited')

The Right Architecture for High Volume

If you genuinely receive thousands of webhooks per minute, don't synchronously process — ingest into a queue. The webhook handler does only: verify signature, write event to queue, return 200. A worker fleet drains the queue at your handler's pace. This decouples receive rate from process rate, so retry storms are absorbed by the queue, not 429s.

How to Reproduce

Use WebhookWhisper's Test Sender to fire 100 deliveries in rapid succession. If your endpoint 429s, your limit is set lower than the test rate. Adjust the limit, re-test until you can absorb the legitimate burst pattern your provider uses.

Frequently Asked Questions

Is there a way to ask the provider to slow down?

Generally no. Most providers don't expose a rate-control knob. Stripe lets you reduce subscribed event types; GitHub lets you turn off specific events; Shopify is mostly all-or-nothing. Receiver capacity is your problem.

Should webhooks bypass my rate limiter entirely?

Bypass per-IP limits, yes — providers concentrate traffic on few IPs. But keep some path-level limit (e.g., 1000/min/route) as a DoS guardrail.

My handler returns 429 but the source keeps retrying. Why?

Either you didn't include a Retry-After header (so the source retries immediately) or the source ignores it. Always include Retry-After. If the source still floods you, your fix is queue ingestion, not stricter limits.

Debug This Error in Real Time

WebhookWhisper captures every webhook request with full headers, body, and timing — so you can see exactly what the provider sent and reproduce the error instantly.

Start Debugging Free
Webhook 429 Too Many Requests — Causes & Fixes (2026) | WebhookWhisper