Skip to content
Security

Rate Limiting: Buckets, Windows and the Keys That Actually Matter

The honest three algorithms, layered key design including the abuse limits everyone forgets (per victim, not per attacker), the 429 contract, and operational rules — fail open, shadow mode, degrade before deny.

4 min read Updated Sep 4, 2026
Rate Limiting: Buckets, Windows and the Keys That Actually Matter

Rate limiting is the seatbelt of API design: invisible until the crash, at which point it's the only thing that matters. And "the crash" takes more forms than people plan for — the credential-stuffing bot, the partner's runaway retry loop (their bug, your outage), the scraper, the customer's intern with a while-loop, and occasionally your own mobile app after a bad release. One discipline defends against all of them, and it's cheap. Let's design it properly: algorithms, keys, responses, and the operational layer nobody documents.

Token bucket and sliding window algorithms feeding per-key limits and Retry-After responses

Algorithms: the honest three

  • Fixed window — a counter per key per minute (INCR + EXPIRE). Trivial, memory-cheap, and famously allows 2× bursts straddling window edges (100 at 11:59:59 + 100 at 12:00:01). For most limits, honestly: fine. The boundary burst rarely matters at application scale.
  • Sliding window — either the log version (a sorted set of timestamps, exact but memory-proportional-to-traffic) or the elegant counter approximation: weight the previous window's count by its remaining overlap. One extra GET, smooth limiting, no burst artifact — my default for public APIs.
  • Token bucket — capacity + refill rate, allowing controlled bursts above the sustained rate ("120 burst, 60/min sustained"). This matches how legitimate clients actually behave — bursty page loads, batch syncs — which is why it's what I use where UX matters. Implement atomically in Redis with a few lines of Lua (read-modify-write on two keys must not race).

Framework note for the Laravel crowd: RateLimiter gives you fixed-window per named limiter out of the box, and it's the right 80% answer — reach for hand-rolled buckets only where burst-shaping genuinely matters.

Keys: what you limit matters more than how

The algorithm debate is fun; the key design is where limits succeed or fail. Real systems layer several:

Layer 1  Per IP, coarse       edge/WAF — bot-storm insurance; generous, because
                              NAT and CGNAT put thousands of humans behind one IP
Layer 2  Per credential       the workhorse: per API key / user / tenant —
                              aligned to whatever you bill or trust
Layer 3  Per credential+route expensive endpoints get their own budget:
                              search ≠ status-check; exports ≠ everything
Layer 4  Per resource         the sneaky one: 5 OTP requests per PHONE NUMBER,
                              3 resets per ACCOUNT — abuse limits keyed to the
                              victim, not the attacker (who rotates IPs freely)

That fourth layer is the one audits find missing: login attempts limited per-IP only (attacker: hello, botnet), OTP sends unlimited per target (attacker: SMS-bombs your own users at your expense). Abuse limits key on the resource being abused. Also deserving separate buckets: auth/reconnect endpoints (so a stampede degrades gracefully) and unauthenticated routes generally (strict by default — anonymous traffic has no reputation to lose).

The response: a contract, not a door slam

HTTP/1.1 429 Too Many Requests
Retry-After: 23
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 23

{ "type": ".../problems/rate-limited", "title": "Rate limit exceeded",
  "detail": "100 requests per minute per API key.", "retry_after": 23 }

Always 429 (never 403 — clients branch on this, per the API checklist), always Retry-After (well-behaved clients honor it; you're literally scheduling your own load), and rate headers on successful responses too, so clients can self-throttle before hitting the wall. Document limits publicly — secret limits don't prevent abuse, they prevent partners from building compliant clients.

Operational wisdom, bought at retail price

  • Fail open, loudly. Redis down should mean "limits off + alert screaming", not "site down". The limiter protects availability; it must never become the availability problem. (Exception: abuse-critical limits like OTP may justifiably fail closed — decide per limit, on purpose.)
  • Launch in shadow mode. Count and log would-be 429s for two weeks before enforcing. Every rollout skips this once, 429s their biggest customer's integration, and never skips it again.
  • Monitor the limiter itself: 429 rate per key (top-ten list = your abuse dashboard and your "who needs a bigger plan" sales lead list), plus limiter latency — it's on every request's hot path.
  • Tiers belong to config, not code: per-plan multipliers resolved at request time, so sales can sell a higher limit without a deploy.
  • Degrade before denying where product allows: past the soft limit, serve cached/stale responses or queue the work; hard-deny only past the abuse threshold. The best rate limit is one legitimate users never feel.

The whole apparatus — buckets, layers, headers, shadow mode — fits in a few hundred lines against infrastructure you already run. What it buys is disproportionate: the difference between "one partner's retry loop" being a Tuesday log line versus a postmortem. Seatbelts, as noted, are cheap right up until they're priceless.

API getting hammered — or about to be launched into a world that hammers? Rate-limit design reviews pair beautifully with the API checklist; bring your traffic graphs.

Keep reading

Related articles

Security 5 min read

Secrets Management: Life Beyond the .env File

The real leak vectors ranked, the non-negotiables at any size, a three-rung maturity ladder that mostly stops at rung two, and rotation as a rehearsed muscle with two-key overlap.

Security 5 min read

OAuth2 and OIDC in Plain Words

The founding delegation story, the cast renamed into English, the one flow that survived (auth code + PKCE) with every step's attack labeled, ID vs access tokens untangled, and the do-you-even-need-this triage.

Security 4 min read

JWTs vs Sessions: The Debate, Settled-ish

Why revocation is where stateless goes to die, the XSS blast-radius problem, the specific habitats where JWTs genuinely shine, and the sessions-at-the-edge / tokens-between-machines architecture.