Benchmarks
Hit-path cost, five load scenarios and garbage-collection cost, each with its machine, its command and what it does not show.
Every number on this page was measured on one machine, in process, with a synthetic origin. They tell you what Weir costs and whether its limits hold under load. They do not tell you what your deployment will do. The tables are rendered from docs/benchmarks.md in the repository, where the raw output files are linked.
The reference machine
Apple M4 Pro, 12 cores, 24 GiB, macOS. Go 1.27.1 unless a section says otherwise. Absolute figures belong to this machine. On a 4-vCPU Linux container the same hit costs roughly two and a half times as long, and that box drifts by 10 to 20% between runs.
One cached hit
BenchmarkServeHitSmall measures Engine.Serve on a fresh 1 KiB entry in the memory store: classify the request, look it up, check freshness, build the response. No network, no TLS, no net/http.
Two back-to-back runs on the M4 Pro, machine otherwise idle, three builds interleaved by benchstat (-count 10 each; raw: benchmarks/hit-path-opt-m4pro.txt). An earlier run with several Claude Code sessions and a Chrome video stream open had a ±38% spread and is discarded.
| Build | Run 1 ns/op | Run 2 ns/op | vs M1 (run 1 / run 2) | B/op | allocs/op |
|---|---|---|---|---|---|
M1 73e4d2e | 870.5 ± 1% | 854.1 ± 2% | 1 480 | 14 | |
M10 b909550 | 1 087 ± 3% | 1 070.5 ± 1% | +24.9% / +25.3% | 1 480 | 14 |
| optimized branch | 833.5 ± 3% | 821.4 ± 4% | -4.3% / -3.8% (p<0.01) | 1 536 | 12 |
The optimized hit path is about 4% faster than M1 and 23% faster than M10 on the M4 Pro, with two fewer allocations. The +32% growth since M1 that the first reference run exposed is gone. Bytes per op are 56 above M1 (1 536 against 1 480), not the 160 above M1 of the release candidate, and the allocation count is lower.
The gain is smaller on the M4 Pro than on the Xeon (-30% there against M10-to-branch -23% here, relative to the release candidate), as expected: SHA-256 is cheaper with the ARM instructions, so the two tags no longer hashed per hit saved less.
New baseline. The 20% regression rule is measured from this run, a median of about 0.83 µs. NFR-5 sets the ns/op ceiling at 1.5 times that, 1.25 µs (docs/01), replacing the 1.9 µs derived from the first, noisier reference run. The first reference run above stays as the record of the release candidate.
After the last hardening changes before v1.0.0 the same benchmark measured 852.2 ns with 12 allocations on Go 1.27.2, with the machine not idle. The project's own budget is at most 1.25 µs and 16 allocations per hit on this machine, and a change that slows the hit path by more than 20% fails review.
make bench
Five load scenarios
make load runs each failure mode against a real engine for about eight minutes in total and checks a threshold. This is the idle reference run of 2026-10-11.
| Scenario | Measured | Threshold | Result |
|---|---|---|---|
| steady hits | 26.3 M requests, 438 k/s achieved (target 442 k/s, 50% of saturated), hit ratio 1.0000, p50 2.0 µs, p99 18.4 to 22.5 µs in each of six windows, 0 origin calls after warm | ratio > 99%, median-window p99 < 200 µs | pass |
| synchronized expiry | 10 000 keys stored in 1.7 s, max-age=60, all 10 000 refetched, origin peak in-flight 32, busiest second 1 485 origin calls, 0 client errors | refetch ≥ 90%, in-flight ≤ 32, ≤ 2 000 calls/s | pass |
| busting flood plus normal | 99 976 flood requests (71 151 shed), flood in-flight peak 8 (MaxPerPartition 8), normal offered 499/s with 0 drops, normal p99 36.9 µs alone and 41.0 µs under flood (+11%) | in-flight ≤ 8, p99 within 20% (50 µs floor) | pass |
| origin brownout (×20) | 18 breaker open events, ended closed, 52 975 stale responses, every request 2xx before, during and after, peak 48 goroutines | breaker per config, stale served, no goroutine growth | pass |
| store flap (50%) | 10.8 M requests, 0 client errors, slowest request 47.1 ms (bound 208.8 ms), store breaker 14 open / 8 closed | bounded latency, breaker cycles | pass |
All five passed, and TestGCAt1MEntries passed as well.
What each one shows:
- Synchronized expiry. 10,000 entries with the same
max-ageexpire together. The limiter holds origin concurrency at its configured 32 and every entry is refetched. - Busting flood. One path is flooded with distinct queries while other paths carry normal traffic. The flood is held to its per-path cap of 8, and normal requests are 11% slower at p99.
- Origin brownout. The origin fails in bursts. The breaker opens and closes, stale content covers the gaps, and no client sees an error.
- Store flap. The store fails half its calls. Requests fall through to the origin under the limiter.
Garbage collection at one million entries
The memory store keeps entries as ordinary Go objects, so the garbage collector has to walk them. This measures what that costs per request as the store grows.
| 10 000 entries | 1 000 000 entries | |
|---|---|---|
| GC CPU per forced cycle | 9.6 ms | 1 299 ms |
| paced p50 / p99 (forced cycle every 5 s) | 4.6 µs / 36.9 µs | 3.8 µs / 36.9 µs |
| paced slowest request | 2.0 ms | 11.9 ms |
| saturated requests in 60 s | 61.4 M | 43.4 M |
| allocation per request | 1 816 B | 1 824 B |
| non-GC CPU per request (generator included) | 11.0 µs | 14.7 µs |
| natural cycles in the window | 4 127 | 36 |
| GC share of busy CPU, runtime-reported | 3.1% | 6.2% |
| GC share of busy CPU, projected | 5.7% | 6.8% |
| GC µs per request | 0.66 | 1.08 |
The D36 gate (at most 2 µs of GC per request at 1M entries) holds with 1.08 µs, against 1.76 µs on the 4-core Xeon. The share of busy CPU is 6.2% to 6.8%, under the 10% line that PLAN M10.5b set. The pointer-light layout (M16-02) stays deferred.
At 1 million entries the collector costs 1.08 µs per request here, and 1.76 µs on the 4-vCPU Linux container, against a project gate of 2 µs. A layout that stores entries without pointers brought that to 0.014 µs in a prototype, but it added 12 allocations per hit and broke the hit-path budget, so it is deferred (M16-02) until a deployment needs it. GOGC=200 roughly halves the cost for more memory headroom.
What this does not show
- No comparison with any other cache. We have not run one, so the comparison page is qualitative.
- No network, TLS or HTTP parsing. A request served through
net/httpover TLS costs many times the 852 ns measured here, and most of that is outside Weir. - No real origin. The synthetic origin answers instantly or fails on cue.
- One reference machine and one container. No ARM server, no NUMA box, no production trace.
- The Valkey store is covered by correctness tests, not by these load scenarios.