Rate Limiting Explained: 5 Algorithms You Actually Need to Know

Rate Limiting Explained: 5 Algorithms You Actually Need to Know

Share
System DesignBackend EngineeringRate Limiting
10 min read

Your server is not infinite. I know that sounds obvious, but a surprising number of system designs act like it isn’t.

Every machine has a ceiling — a point where more requests means slower responses, degraded performance, or a full crash. Rate limiting is what you put between your users and that ceiling. It’s one of those topics that gets hand-waved in beginner tutorials (“just add rate limiting!”) but deserves a real breakdown, because the algorithm you pick matters more than you’d think.

Here’s a walk-through of five common rate limiting strategies — what they do, how they work, and where each one starts to break down.

What Is Rate Limiting, Actually?

Say your server can handle 100 requests per minute. Not exactly 100 — there’s no hard cliff where request 101 causes an immediate crash — but beyond that rough threshold, performance degrades, the machine overheats, and eventually things fall over.

Users don’t know or care about this. They’ll hammer your endpoints as fast as they want. So you put a rate limiter in front of your server: a layer that checks incoming requests against your threshold and either lets them through or drops them.

When a request gets dropped, you return HTTP 429 Too Many Requests — the standard signal that the client should back off and retry later.

That’s the concept. Now the interesting part: how do you count and enforce that limit?

Algorithm 1: Token Bucket

This is probably the most widely used approach. Amazon and Stripe both use it for API throttling.

The idea: You have a bucket with a fixed capacity. A background process refills it with tokens at a steady rate. Every incoming request consumes one token. No token? Request gets dropped.

How it plays out:

  • Bucket capacity: 5 tokens
  • Refill rate: 3 tokens per second
  • At second 1: bucket fills to 3 tokens
  • Three requests arrive → 3 tokens consumed → bucket is empty
  • A fourth request arrives → no tokens → 429, rejected
  • At second 2: bucket refills to 3 → requests can flow again
image

Algorithm 1 — Token Bucket Each incoming request must claim a token from the bucket. No token, no entry. The refiller runs in the background at a fixed rate — your server never sees more than it can handle.

One thing worth noting: the bucket never exceeds its capacity. If no requests come in for two seconds, you still cap at 5 tokens, not 6 or 9.

It allows short bursts of traffic up to the bucket size, which matches how real users actually behave — not a perfectly metered drip, but occasional spikes.

Where it struggles: Two parameters to tune — bucket size and refill rate — and getting them right for production traffic is genuinely hard. Too conservative and you’re rejecting legitimate requests. Too generous and the protection barely means anything.

Algorithm 2: Leaky Bucket

Same core intuition, different angle. Think of a bucket with a small hole at the bottom.

Requests pour in from the top at whatever rate they arrive — fast, slow, bursty, doesn’t matter. The hole drains them out at a fixed rate, regardless of how fast they came in. This is implemented as a FIFO queue.

A good analogy: your overhead tank holds 1,500 liters, but the showerhead only lets water out at a comfortable flow. The showerhead controls the rate, not the tank.

What this looks like in practice:

  • Requests arrive at 20,000/minute
  • Your server can process 5 requests/minute
  • Leaky bucket accepts the incoming flood, queues requests, releases them at the server’s actual pace
  • If the queue fills completely, then you start returning 429
image

Algorithm 2 — Leaky Bucket Requests pile in at any speed, but only leave at the rate your server can absorb. The queue is the buffer. When the buffer fills, the rest get dropped.

Good for use cases where you need consistent, predictable throughput — payment processing, database writes, anything where a sudden flood to the backend would cause problems even if each individual request is legitimate.

Where it struggles: If a burst of traffic floods in, it fills the queue with old requests, and newer ones never get processed. It also has the same two-parameter tuning problem as token bucket: queue capacity and drain rate.

Algorithm 3: Fixed Window Counter

This one is intuitive and easy to implement. It also has a notable bug, which is why you need to know the next two algorithms.

The idea: Divide time into fixed windows — 1 second, 1 minute, whatever fits your rate. Count requests within each window. When the count hits your threshold, reject the rest until the next window starts.

Example with threshold of 3 requests/second:

  • Window 1 (0s–1s): 3 requests come in → all processed → window closes, counter resets
  • Window 2 (1s–2s): 2 requests → both fine → 3 more arrive, 1 gets through, 2 rejected
image

Algorithm 3 — Fixed Window Counter: Shows three time windows with counters, and a highlighted red zone exposing the boundary spike bug — 6 requests sneaking through in 200ms across two “clean” windows.

The bug: Consider what happens at window boundaries. A user sends 3 requests at t=0.9s (end of window 1) and 3 more at t=1.1s (start of window 2). Both windows show clean counts within the limit. But in real time, 6 requests just hit your server within 200ms. Your counter said everything was fine. Your server didn’t agree.

This is the boundary spike problem, and it’s not a theoretical edge case — it’s exactly how traffic looks when someone is probing or hammering your API.

Algorithm 4: Sliding Window Log

This algorithm directly fixes the boundary problem.

Instead of resetting a counter at fixed intervals, you keep a log of timestamps for every accepted request. When a new request arrives:

  1. Remove all timestamps older than your window size
  2. Count what’s left
  3. If count < threshold → accept and add the new timestamp
  4. If count ≥ threshold → reject

Example with a 5-second window, threshold of 3:

  • Request at t=1 → log: [1] → count 1 → accepted
  • Request at t=3 → log: [1, 3] → count 2 → accepted
  • Request at t=4 → log: [1, 3, 4] → count 3 → accepted
  • Request at t=6 → t=1 expires (outside the 5s window) → log: [3, 4] → count 2 → accepted
  • Another request at t=7 → log: [3, 4, 6] → count 3 → rejected
image

Algorithm 4 — Sliding Window Log Every accepted request leaves a timestamp. The window slides forward in real time, evicting expired entries as it goes. No fixed boundary means no boundary to exploit.

The window slides with time rather than snapping to fixed boundaries. No way to exploit the edge.

Where it struggles: Memory. You’re storing a timestamp per request, which gets expensive fast. At any meaningful scale, that’s a lot of data to keep in Redis. Fine for low-volume critical paths (login rate limits, password resets), harder to justify for high-throughput general APIs.

Algorithm 5: Sliding Window Counter

A hybrid of algorithms 3 and 4 — aimed at getting sliding window accuracy without the memory cost.

The idea: Keep fixed windows with counters. When a new request arrives partway through the current window, weight the previous window’s count by how much of it overlaps with your current window.

How the math works:

  • A request arrives 30% into the current window
  • You use: (current window’s request count) + 70% of (previous window’s count)
  • If that weighted total is below threshold → accept

Example:

  • Threshold: 3 requests per second
  • Previous window: 3 requests
  • New request arrives at the 20% mark of the new window
  • Weighted count = (current count) + 0.80 × 3
  • If total ≥ 3 → reject
image

Algorithm 5 — Sliding Window Counter: Shows the hybrid math in action — 30% into the current window → weight previous window at 70% → 2.1 + 1.0 = 3.1 → rejected. Clean and numeric.

You’re only ever storing two counters — previous window and current window — instead of individual timestamps. Much cheaper at scale.

Where it struggles: It’s an approximation. There are edge cases where it slightly over- or under-counts. For most applications the margin is fine, but if you need guaranteed precision (security-critical rate limits), sliding window log is still the right call.

Side-by-Side Comparison

image

Which One to Pick

Token Bucket is a solid default for most APIs. Simple, battle-tested, handles normal burst patterns.

Leaky Bucket fits when consistent throughput matters more than raw throughput — payment systems, queue-fed workers, anything where a sudden spike to the backend would cause downstream problems even if the requests themselves are valid.

Fixed Window Counter is fine for low-stakes limits where occasional edge-case spikes don’t really matter. Don’t use it anywhere that boundary exploits would be a real concern.

Sliding Window Log is the right call when precision matters and memory is not a constraint. Authentication rate limits are the obvious example — a boundary exploit there isn’t just a performance issue.

Sliding Window Counter is usually the best production choice when you need accuracy at scale without the memory overhead of logging every timestamp. Most large-scale rate limiting systems end up here.

In practice, Redis is almost always involved. Libraries like rate-limiter-flexible (Node.js) or django-ratelimit abstract most of this away. But when something behaves unexpectedly under traffic — and it will — knowing what's running underneath is what lets you actually fix it.

Rate limiting isn’t the most exciting part of system design. It’s also one of the things that separates a system that survives a traffic spike from one that pages you at 2am.

Share this article