Skip to content
About
Main content

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 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 (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 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

With bucket capacity C = 500 and a client that has been quiet, how many of the 200 burst requests does the token bucket accept?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    token-bucket-versus-fixed-window.py53 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat 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

100 requests at 0.95 s and 100 at 1.05 s
LimiterAccepted
fixed window (100 per second)200
token bucket R=100/s, C=100110
token bucket R=100/s, C=500200
sliding window counter105
Reference runApple M2 · 8 cores · 8 GB
Output · uv run token-bucket-versus-fixed-window.py
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 s
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What 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.