Rate limiting rollout plan
We're seeing bursts of 4–5k requests/min from a handful of integrations, and the p99 is paying for it. This is the plan we aligned on: token bucket at the edge, per-key limits, and a two-week grace period with warning headers before we enforce.
Why token bucket
- Absorbs honest bursts (webhook retries, batch jobs) without punishing them
- One counter per key — cheap to run in Redis
- Degrades predictably: when the bucket is empty you wait, nothing breaks
Fixed windows looked simpler, but they double-charge clients whose bursts straddle the window edge. Token bucket costs one
INCRmore and none of the support tickets.
Proposed limits
| Plan | Sustained | Burst | Overage response |
|---|---|---|---|
| Free | 60 req/min | 120 | 429 + Retry-After |
| Pro | 600 req/min | 1,200 | 429 + Retry-After |
| Enterprise | Custom | Custom | Contact us |
Edge check (simplified)
export async function checkLimit(key: string, cost = 1) {
const bucket = await redis.hgetall(`rl:${key}`)
const tokens = refill(bucket, limits[bucket.plan])
if (tokens < cost) {
return { ok: false, retryAfterMs: msUntilNextToken(bucket) }
}
await redis.hincrbyfloat(`rl:${key}`, 'tokens', -cost)
return { ok: true }
}
Rollout checklist
- Shadow mode: log would-be 429s for two weeks
- Add
X-RateLimit-*headers to every response - Email the twelve integrations currently over the Pro limit
- Enforce on Free plan (March 3)
- Enforce on Pro plan (March 17)
Open questions
- Do webhook deliveries count against the same bucket, or get their own?
- Should
Retry-Afterjitter be server-side or documented client guidance?
Drafted with Claude, cleaned up by hand. Ping #platform with objections before Friday.