Architecture patterns
Token bucket
A bucket that holds tokens and refills over time, giving you two dials for a sustained rate and a burst size. The most common shape for API limits.
A token bucket holds up to a fixed number of tokens. Tokens refill at a steady rate. Each request takes one token, or several for an expensive request. If the bucket is empty, the request is rejected. Two numbers control it: how fast tokens refill, and how many the bucket holds.
The big idea
Think of a water tank with a slow tap. The tap refills it at a steady rate. You can draw a big cup at once if the tank is full, but after that you are limited to the flow from the tap.
The two dials match product language. The refill rate is the sustained throughput you promise. The capacity is the burst you tolerate. A client that has been idle for a while has a full bucket and can spike. A client that keeps pushing is held to the refill rate.
The analogy breaks at one point. A tank is physical, but a bucket in a rate limiter is just two numbers and a timestamp. You never run a timer. You compute the refill from the time elapsed when a request arrives.
Walking through it
This version takes the time as an input so the output is repeatable:
class TokenBucket {
private tokens: number
private last = 0
constructor(private capacity: number, private refillPerSec: number) {
this.tokens = capacity
}
take(nowMs: number, cost = 1): boolean {
const elapsedSec = (nowMs - this.last) / 1000
this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillPerSec)
this.last = nowMs
if (this.tokens < cost) return false
this.tokens -= cost
return true
}
}
const bucket = new TokenBucket(50, 10) // burst of 50, sustained 10 per second
let burst = 0
for (let i = 0; i < 55; i++) if (bucket.take(0)) burst++
console.log("t=0s allowed:", burst)
let later = 0
for (let i = 0; i < 15; i++) if (bucket.take(1000)) later++
console.log("t=1s allowed:", later)Output:
t=0s allowed: 50
t=1s allowed: 10At t=0 the bucket starts full, so 50 requests pass and the rest are rejected. One second later, 10 tokens have refilled, so exactly 10 more pass. Over any interval of length T, at most capacity + rate × T requests get through.
A leaky bucket is the same idea seen from the output side. Requests drain at a fixed rate, and overflow is dropped. The token bucket lets bursts through immediately. The leaky bucket smooths them out.
Common mistakes
- Setting capacity equal to the average rate. Clients that batch their calls get throttled even though their average is fine. Give the bucket room for a burst.
- Making the burst too large. One client can drain a shared backend in a single spike.
- Charging every request the same. Use
costfor expensive requests, so one heavy call uses more tokens.
Check yourself
-
A bucket holds 50 tokens and refills 10 per second. It is empty at t=0. How many requests pass at t=2 s?
Answer: 20. Two seconds of refill at 10 per second is 20 tokens.
-
Why does the bucket need no timer?
Answer: it works out the refill from the elapsed time whenever a request arrives.
Key takeaways
- A token bucket has two dials: a sustained rate (refill) and a burst size (capacity).
- It allows bursts up to the capacity, then holds the client to the refill rate.
- Refill is computed lazily from elapsed time, so no background timer is needed.
- Use
costto make expensive requests draw more tokens.
Further learning
Next, see what changes when many servers share one bucket. The distributed limiter on Redis covers the race condition and the atomic script that fixes it.
Related
Rate limiting
Why systems cap how much work a client can send, which algorithm counts, where the limit lives and what the client is told. Start here for the rate-limiting map.
Window counting limiters
Fixed windows, sliding logs and sliding counters, and the boundary burst that makes the simplest one leak.
Distributed limiter on Redis
How many servers share one rate-limit count without races, using an atomic Redis Lua script, one clock, expiring keys and a deliberate failure mode.