Token bucket versus fixed window
Simulate a modelNeeds Python
Problem
A rate limiter decides how many requests to admit per unit of time, and the way it counts decides what bursts get through. A fixed window counts per calendar second and resets at the boundary, so a burst at the end of one second and another at the start of the next both fit, twice the limit in a moment. A token bucket refills continuously at rate R up to a capacity C, so it admits at most C at once plus what refills. A sliding window weights the previous window by how much of it still overlaps.
Implement a fixed window counter (limit 100 per second) and a token bucket (R = 100/s, C = 100). Send 100 requests at 0.95 s and 100 more at 1.05 s. Then change C to 500 and observe that a quiet client can burst 500 once. Optional: implement the sliding window counter and show that it removes the boundary spike.
The question. Of 200 requests in 0.1 s across a window boundary, how many does each limiter accept?
Predict
Expect the fixed window to accept all 200 (two bursts straddling the boundary, so 200 in 0.1 s) and the token bucket to accept about 100 plus the 10 tokens refilled in 0.1 s.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
token-bucket-versus-fixed-window.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Token bucket against fixed window. 100 requests at 0.95 s and 100 more at 1.05 s, against a fixed-window counter (100 per second), a token bucket (R = 100/s, C = 100), a token bucket with C = 500, and a sliding-window counter. """ import argparse, json p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--limit", type=int, default=100, help="Requests per second allowed.") p.add_argument("--burst", type=int, default=100, help="Requests in each burst.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() L = args.limit requests = [0.95] * args.burst + [1.05] * args.burst def fixed_window(): counts, ok = {}, 0 for t in requests: w = int(t); counts[w] = counts.get(w, 0) + 1; ok += counts[w] <= L return ok def token_bucket(capacity): tokens, last, ok = capacity, 0.0, 0 for t in requests: tokens = min(capacity, tokens + (t - last) * L); last = t if tokens >= 1: tokens -= 1; ok += 1 return ok def sliding_window(): """Weighted count: this window's count plus the previous window's, weighted by its overlap.""" counts, ok = {}, 0 for t in requests: w = int(t); prev = counts.get(w - 1, 0); overlap = 1 - (t - w) if counts.get(w, 0) + prev * overlap < L: counts[w] = counts.get(w, 0) + 1; ok += 1 return ok rows = [{"limiter": "fixed window (100 per second)", "accepted": fixed_window()}, {"limiter": "token bucket R=100/s, C=100", "accepted": token_bucket(L)}, {"limiter": "token bucket R=100/s, C=500", "accepted": token_bucket(5 * L)}, {"limiter": "sliding window counter", "accepted": sliding_window()}] for r in rows: print(f"{r['limiter']:<32} accepted {r['accepted']:>3} of {len(requests)} in 0.1 s") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"limiters": rows}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run token-bucket-versus-fixed-window.py uv run token-bucket-versus-fixed-window.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --limit100 | Requests per second allowed. |
| --burst100 | Requests in each burst. |
Our run
100 requests at 0.95 s and 100 at 1.05 s
| Limiter | Accepted |
|---|---|
| fixed window (100 per second) | 200 |
| token bucket R=100/s, C=100 | 110 |
| token bucket R=100/s, C=500 | 200 |
| sliding window counter | 105 |
fixed window (100 per second) accepted 200 of 200 in 0.1 s
token bucket R=100/s, C=100 accepted 110 of 200 in 0.1 s
token bucket R=100/s, C=500 accepted 200 of 200 in 0.1 s
sliding window counter accepted 105 of 200 in 0.1 sWhat we found
The fixed window accepted 200 requests in a tenth of a second, twice its limit. The token bucket with C = 100 accepted 110: its 100 tokens plus the 10 refilled in 0.1 s, as expected. With C = 500 it accepted 200, because a quiet client had saved up the burst. The sliding window accepted 105, removing the boundary spike.
Read it
The capacity C is a burst allowance, and it is a promise to the client: a large C lets a quiet client send a large burst at once. Choose it from what the downstream can absorb in a moment, not from the average rate, and prefer a limiter whose count does not reset at a boundary.