Evidence
The rate-limit case against a real limiter
Historical experiment (2026-09-27). Its machines, installed tools, and deployment topology describe that run, not vero prerequisites. For current setup see Getting started and Running the examples.
Chapter 10.4 calls this the most valuable experiment the dossier did not run: its 3% came from a
token bucket with no time window, and a real limiter with a window and a burst allowance might be
more forgiving, which would weaken the case for holds. This is one data point, nginx
limit_req, run on 2026-09-27 between 07:05:31Z and 07:05:35Z.
Result: it does not weaken the case. Against nginx, four edge-less requests racing for the
budget passed 135 of 300 runs at --jobs 8 and 300 of 300 at --jobs 1; the loser was whichever
request arrived last, spread over all four positions. The serial plan with holds passed 300/300.
And without a reset between runs, the second run onward gave [429 429 429 429] every time, as
chapter 7.1 measured. The failure stays schedule-dependent under a real limiter.
Historical setup
- Limiter:
docker.io/library/nginx:1.27-alpine, pulled asnginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10, one worker process, on the k3s cluster in namespacetesting-platform, fromdeploy/experiments/real-limiter/. - Config, the part that matters:
One leaky bucket perlimit_req_zone $http_x_api_key zone=perkey:10m rate=1r/m; limit_req_status 429; location /orders { limit_req zone=perkey burst=2 nodelay; try_files /orders.json =404; }X-Api-Key, one request a minute with a burst of two served at once, so four requests in quick succession give200 200 200 429. Checked by hand with curl on one key:200 200 200 429 429. (return 200would not do:returnruns beforelimit_reqsees the request, so the file is served withtry_files.) - Path: vero in the
workercontainer → the veth gateway$POD(10.233.1.1) → NodePort 30561 → kube-proxy → the pod. A NodePort, notkubectl port-forward. No real network latency: this is a single node, so like the dossier's numbers it measures ordering, not a WAN. - vero at commit
edb9a03plus this experiment's plans, go1.26.7. - Plans in
testdata/experiments/real-limiter/. nginx has no reset endpoint, so variants A and B use a fresh key per run (X-Api-Key: {{ run.id }}, lifecycleunique); variant C uses one fixed key for every run.
Historical commands — do not use as current setup
Current-tree warning: make infra-up now deploys Postgres using NodePort 30561, the same port
this limiter used. Do not follow the recorded command block as current setup: the deployments
would collide. The block below is a transcript of the experiment, not an install recipe.
make infra-up # the namespace
kubectl apply -k deploy/experiments/real-limiter
export VERO_LIMITER_URL=http://$POD:30561 VERO_LIMITER_KEY=g02-fixed-<unix time>
bin/vero run --jobs 8 --repeat 300 --events a.ndjson testdata/experiments/real-limiter/serial-holds.yaml
bin/vero run --repeat 300 --compare-jobs --jobs 8 --events b.ndjson testdata/experiments/real-limiter/four-edgeless.yaml
bin/vero run --jobs 1 --repeat 300 --events c.ndjson testdata/experiments/real-limiter/serial-no-reset.yaml
kubectl delete -k deploy/experiments/real-limiter
The 429 positions were counted from the request events of each run in the event logs.
Results
| Variant | Runs | Passed | Where the 429 landed |
|---|---|---|---|
A. Serial repeat of 4, task holds the key (B06's shape), --jobs 8 |
300 | 300 | fourth request, 300 times |
B. Four edge-less tasks (E07's shape), --jobs 1 |
300 | 300 | fourth task, 300 times |
B. Four edge-less tasks, --jobs 8 |
300 | 135 (45%) | first 28, second 47, third 90, fourth 135 |
C. Serial, fixed key, no reset between runs, --jobs 1 |
300 | 1 | run 1: [200 200 200 429]; runs 2 to 300: [429 429 429 429] |
Every B run had exactly one 429. vero's --compare-jobs output for B flagged all four tasks
FLAKY at --jobs 8 only and printed the holds hint for each.
Against the dossier
| Dossier (token bucket, no window) | vero-testserver (same, E07) | nginx limit_req (this run) |
|
|---|---|---|---|
| Parallel, pass rate | 10/300 (3%) | 124/300 (41%), --jobs 8 |
135/300 (45%), --jobs 8 |
| Where the 429 landed | 69 / 42 / 58 / 31 over 200 trials | 49 / 72 / 55 / 124 | 28 / 47 / 90 / 135 |
| Serial, pass rate | 50/50 | 50/50 (B06), 300/300 | 300/300 |
| Serial, no reset | [429 429 429 429] |
not run | [429 429 429 429] 299/300 |
The parallel pass rate is much higher than the dossier's 3% with either limiter, and about the same with both. That points at the client, not the limiter: vero's dispatcher starts the four ready tasks in plan order microseconds apart, so the fourth is more often the last to arrive, where the dossier raced four bare goroutines. The limiter type barely moved it (41% to 45%).
Direction of the effect
- The case for
holdsandrepeat.mode: serialstands. A leaky bucket with a burst allowance did not make the racing version reliable: it failed 55% of the time at--jobs 8, never at--jobs 1, and the losing request was spread across all four positions. Nothing about a real limiter's window made "the fourth request" meaningful when the four were concurrent. - The case for
lifecycleis stronger than the dossier stated it. With a limiter that refills one request a minute, a second run a few milliseconds later is[429 429 429 429], and it stayed that way for 299 runs. A plan against a real limiter needsuniquekeys or aresethook; a refill window does not rescue it at test speed. - What this does not show. Everything ran on one node. Over a real network with jitter, arrival order is noisier and the parallel pass rate may move toward the dossier's 3% or away from it; either way it stays a draw the plan does not control. Envoy, Kong and cloud gateways were out of scope.
Historical teardown
kubectl delete -k deploy/experiments/real-limiter removed the ConfigMap, Service and Deployment;
the namespace and D03's Postgres stayed.
This page is docs/content/evidence/real-rate-limiter.md in the repository.