Rate-limiter benchmark harness
Rate-limiter benchmark harness
This is the harness behind the measurements in Everything I Got Wrong About Rate Limiting. It runs a Redis 7 primary and replica locally, then exercises the five behaviors discussed in the post.
The results compare strategies on one laptop under Docker. They are useful for the shape of a trade-off, not as production capacity numbers.
Run it
You need Docker with Compose v2 and Python 3.9 or newer.
cd benchmarks/rate-limiter
docker compose up -d --wait
python3 -m venv /tmp/rate-limiter-bench-venv
source /tmp/rate-limiter-bench-venv/bin/activate
pip install -r requirements.txt
python benchmark.py all
docker compose down -v
The primary is exposed on host port 56380 and its replica on 56381, so this stack can run beside the cache-aside benchmark. Override either URL without editing the harness:
export RATE_LIMIT_PRIMARY_URL='redis://127.0.0.1:56380/0'
export RATE_LIMIT_REPLICA_URL='redis://127.0.0.1:56381/0'
Each experiment can also run alone:
python benchmark.py boundary
python benchmark.py race
python benchmark.py replica
python benchmark.py smoothness
python benchmark.py throughput
Use python benchmark.py all --help for every workload control. The defaults are the settings used for the checked-in CSVs.
What each experiment does
boundarydrives the same 100-request quota through a seam-aware burst and a uniform stream. It records the largest admitted count in any rolling two-second window for a fixed-window counter and a two-bucket sliding counter. No timing is widened here; the seam is part of fixed-window’s shape.raceforces 30 expired-window rollovers. At each rollover eight clients collide on a counter/reset pair, then the harness fills the rest of that window and counts how much quota the racing resets erased. The naive implementation is swept across a disclosed 0/5/10/25 ms gap between reading the reset and writing the new state. The Lua implementation is the atomic control.replicastarts each cycle with an expired, full window, temporarily pauses replication by detaching the replica for 0/10/25/50 ms, then counts responses that reject while the primary’s atomic result reports at least 80 of 100 requests remaining. Detaching is deliberate timing amplification for localhost and is plotted as a sweep. The replica is reattached and verified after every cycle.smoothnesssends eight requests per 100 ms against a 50-per-second limit for 30 virtual seconds. It records admitted requests per 100 ms for fixed and sliding counters. The same 2,400-request stream then runs through the sliding counter and an exact sorted-set log; their per-request decision disagreement rate is the approximation metric.throughputuses 16 spawned Python processes and pipelines of 256EVALSHAcalls so one interpreter thread is not the load-generator ceiling. For the two-shard cases, the replica is temporarily promoted to a second primary. Spread keys are assigned bycrc32(key) % shards; the hot-key case keeps every process on shard zero. The service is restored as a replica afterward.
limiters.py contains the implementations under test: aligned fixed window, deliberately non-atomic two-key fixed window, atomic fixed-window Lua, sliding counter Lua, and exact sliding log Lua.
Results
The harness writes these files under results/:
boundary.csvrace.csvrace_gap_sweep.csvreplica.csvsmoothness_timeseries.csvsliding_accuracy.csvthroughput.csvrun_metadata.csv
The checked-in files came from one complete python benchmark.py all run. Host timing moves between runs, especially throughput; run_metadata.csv records the machine and every important knob used by the article.