ulearn/systems

topics / traffic-routing

Rate Limiter

Fixed window, sliding window, token bucket and leaky bucket throttling, keyed per client. Hammer one caller to see it kick in — then flood from thousands of spoofed IPs to see where per-client limiting alone stops helping.

What a rate limiter actually does

A rate limiter sits in front of an API and decides, per request, whether the caller has budget left — if not, it rejects the request immediately with a 429 Too Many Requests instead of letting it reach the backend at all. Where a load balancer decides which backend handles a request, a rate limiter decides whether it gets handled at all.

Throttling algorithms

Fixed window

Counts requests in a fixed-size time bucket (e.g. one 4-second window) and resets to zero when the window rolls over. Simple and cheap, but bursty at the edges — a client can spend its whole budget in the last instant of one window and again in the first instant of the next, doubling its effective rate right at the boundary.

Sliding window

Keeps a log of recent request timestamps and only counts the ones still inside the trailing window, so the window moves with every request instead of resetting on a fixed clock tick. Fixes the boundary-burst problem, at the cost of remembering every timestamp rather than a single counter.

Token bucket

Each client has a bucket of tokens, refilled continuously up to a capacity. A request costs one token; no tokens, no request. Refilling continuously (rather than resetting all at once) is what lets it absorb a short burst — up to a full bucket's worth — while still capping the long-run average rate.

Leaky bucket

The mirror image of token bucket: requests fill the bucket up, and it drains at a steady rate on its own. If a request would overflow the bucket, it's rejected. Where token bucket smooths the rate a client is allowed to send at, leaky bucket smooths the rate the backend actually sees — bursts get queued and released steadily rather than passed straight through.

Keying: who is “one client”?

Every algorithm above needs a key to count against — usually the caller's IP address, an API key, or an authenticated user ID. The simulation keys by IP. Try “Hammer one client”: it fires a burst of requests from a single fixed address and you'll watch that address get throttled correctly once it exceeds the limit, while every other client keeps its own separate budget untouched.

That per-key isolation is also the mechanism's biggest weakness. It only works if one attacker maps to one key. An API key or logged-in user ID is hard to fake, but a source IP is not — the “Simulate DDoS” button spreads requests across thousands of spoofed addresses, and per-IP limiting lets nearly all of them through, because each spoofed IP looks like a brand-new caller with a full, untouched quota.

Rate limiting vs. a DDoS

This is the same conclusion the load balancer's DDoS section reaches from the other direction: no single layer stops a distributed flood on its own. A load balancer keeps spreading a flood evenly across healthy backends right up until they all fall over. A rate limiter keyed by IP keeps every individual spoofed address under its limit while the aggregate request rate still floods the backend — run the HTTP Flood attack and watch the limiter's 429 count barely move while the API's 503count climbs instead: the limiter waved every spoofed address through individually, and the backend paid for it. Notice the client node itself during the run, too — it sprouts a ring of satellite dots and a distinct-IP counter, because the flood isn't one machine sending a lot of traffic, it's thousands of different ones, each looking like a legitimate first-time caller.

Now turn on the server-wide limiter and run the same flood again. It sits behind the per-client limiter and shares one bucket across every client and spoofed address combined, so rotating source IPs no longer buys the attacker anything — the aggregate rate hits a wall regardless of how many identities it's spread across. Watch the new node's own 429 count climb instead of the API's 503s: traffic gets throttled deliberately, before the backend is ever put at risk, rather than dropped after the fact by running out of capacity. Real systems layer this same idea with something further upstream too — CDN or edge-level filtering, IP reputation, and challenge/CAPTCHA gating — rather than expecting any one mechanism to carry the whole load.

Not every attack reaches the rate limiter

“DDoS” is not one attack, and the four options in the simulator split cleanly into two groups. HTTP Flood and Slowloris are application-layer attacks — real traffic arriving over HTTP, visible in this diagram, putting load on this API. SYN Flood and UDP Amplification operate below that: run either one and watch the packets flash and disappear at the dashed “network” edge, left of the client node — they never enter the pipeline at all, because there's no HTTP request for a rate limiter to inspect in the first place. A SYN flood never finishes the TCP handshake; UDP amplification just reflects volume off third-party servers to saturate your bandwidth. Both are stopped upstream, if at all — by the OS network stack (SYN cookies), a firewall, or a CDN scrubbing traffic before it reaches you — not by anything an application can configure.

Slowloris is the more interesting case precisely because it does reach the app layer but still slips past both limiters here. It never sends a complete request, so a limiter that counts completed requests has nothing to count — run it and watch it skip straight past the Rate limiter and Server limiter nodes entirely, heading for the API's held connection-slot gauge instead. Enough simultaneous slow connections exhaust that pool just as effectively as a flood exhausts request-processing capacity, and no amount of per-request rate-limit tuning touches it — the actual fix is a connection or header-read timeout that kills a socket that's stalled too long, a different mechanism for a different resource.

 Fixed / sliding windowToken bucketLeaky bucket
Allows burstsYes, up to the limitYes, up to bucket capacityNo — smooths output
Memory per clientOne counter (fixed) or a timestamp log (sliding)One float (token count)One float (queue level)
Output rate to backendUneven — can spike at window edgesUneven — bursty within capacitySteady, capped at the leak rate
Typical useSimple API quotasClient-facing APIs that tolerate burstsProtecting a backend that can't handle spikes