On this page
Benchmarks
Disclaimer
Benchmarks are synthetic. They measure isolated behavior in artificial conditions. Production workloads have different allocation patterns, concurrency levels, GC pressure, network conditions, and hardware. The Varnish and NGINX configurations used in these tests could likely be improved — a differently tuned Varnish or NGINX may produce different results. Benchmarking is genuinely hard, and these numbers should be treated as directional indicators, not as definitive performance claims.
Always benchmark on your own hardware with your own workload before making infrastructure decisions.
Methodology
All benchmarks were run on the same machine (Apple M5, darwin/arm64) with Go 1.27.
Proxies were run in Docker containers via docker compose, with an origin server
serving synthetic responses. Load was generated by
k6 at fixed RPS
targets.
Proxies tested
| Proxy | Version | Configuration |
|---|---|---|
| bouine | v0.5.x (current main) | Default config with stale_while_revalidate: 30s |
| Varnish | 7.x | Default VCL with grace period, SWR enabled |
| NGINX | 1.25.x | proxy_cache with default settings |
| Envoy | 1.29.x | HTTP cache filter with default settings |
Scenarios
| Scenario | Description | RPS | VUs | Hit rate target |
|---|---|---|---|---|
| 3.2 Hit-only | Warm cache, all requests are cache hits | 3000 | 200 | 100% |
| 3.3 Miss storm | Cache-Control: no-store on all responses | 1500 | 100 | 0% |
| 3.6 Mixed realistic | 60% cacheable + 40% non-cacheable + revalidation | 3000 | 300 | ~73% (bouine) / ~93% (Varnish) |
The mixed scenario uses a realistic mix of cacheable and non-cacheable responses with
varying TTLs, conditional requests (ETag revalidation), and Vary headers.
Results
Scenario 3.2 — Hit-Only (3000 RPS, 100% cache hits)
Pure cache-hit performance. Every request finds a fresh cached response.
| Proxy | Avg latency | p90 | p95 | RPS achieved |
|---|---|---|---|---|
| bouine | 0.166 ms | 0.219 ms | 0.277 ms | 2996 |
| Varnish | 0.177 ms | 0.201 ms | 0.263 ms | 2999 |
| NGINX | 0.166 ms | — | 0.304 ms | 3000 |
| Envoy | 0.232 ms | — | 0.300 ms | 2999 |
Bouine and NGINX match Varnish on pure cache hits. Envoy is ~40% slower.
Scenario 3.3 — Miss Storm (1500 RPS, all misses)
Every request goes to origin (origin returns Cache-Control: no-store).
| Proxy | Avg latency | p90 | p95 | RPS achieved |
|---|---|---|---|---|
| bouine | 0.157 ms | 0.186 ms | 0.232 ms | 1500 |
| Varnish | 0.166 ms | 0.218 ms | 0.298 ms | 1500 |
| NGINX | 541 ms (overloaded) | 1501 ms | 3002 ms | 1283 |
| Envoy | 0.307 ms | 0.172 ms | 0.189 ms | 1500 |
NGINX’s proxy_cache path is not optimized for the no-store case and degrades under
sustained miss traffic. Bouine and Varnish handle miss storms equally well. Envoy has
low p90/p95 but higher average due to occasional spikes.
Scenario 3.6 — Mixed Realistic (3000 RPS, ~73% hit rate)
The most production-representative scenario: a mix of cacheable and non-cacheable
responses, conditional revalidation, and Vary headers.
| Proxy | Avg latency | p90 | p95 | Hit rate |
|---|---|---|---|---|
| bouine | 0.278 ms | 0.258 ms | 0.334 ms | 72.8% |
| Varnish | 0.233 ms | 0.260 ms | 0.385 ms | 93.0% |
| NGINX | 22.1 ms (overloaded) | — | 0.625 ms | 72.9% |
| Envoy | 0.281 ms | 0.273 ms | 0.354 ms | 0% (no caching) |
Varnish’s 93% hit rate vs bouine’s 73% is the primary driver of the latency difference.
Varnish serves stale objects unconditionally during revalidation (grace mode), while
bouine’s stale-while-revalidate is RFC 5861-compliant (only serves stale within the
SWR window). Aligning bouine’s grace semantics with Varnish’s would close most of this
gap. Envoy’s cache filter did not cache any responses in this scenario (0% hit rate).
Evolution: bouine before and after optimization
| Scenario | bouine (original) | bouine (optimized) | Improvement | vs Varnish |
|---|---|---|---|---|
| Hit-only | 0.338 ms | 0.166 ms | -51% | 2.1x slower → 6% faster |
| Miss storm | 0.647 ms | 0.157 ms | -76% | 4.4x slower → 5% faster |
| Mixed | 0.485 ms | 0.278 ms | -43% | 2.85x slower → 1.19x slower |
Go micro-benchmarks
Unit-level benchmarks isolating specific hot-path components, from the current bench/results/current.txt gates. Handler_CacheMiss_Cacheable is held to an allocation budget of 18 in CI (currently at 13 — see the v0.5.2 changelog).
| Benchmark | ns/op | B/op | allocs/op | Description |
|---|---|---|---|---|
Evaluate_Hit | ~45 | 0 | 0 | RFC 9111 freshness evaluation |
HotStore_Get_Hit | ~18 | 0 | 0 | In-memory cache lookup (SIEVE) |
BuildKey | ~48 | 0 | 0 | Cache key computation (xxhash64) |
FastPath_Hit | ~129 | 0 | 0 | Full H1 fast-path cache hit (parse + lookup + writev) |
H1Parse_Get | ~192 | 0 | 0 | H1 request parse (h1parser) |
Handler_CacheHit_ReusableWriter | ~390 | 0 | 0 | Hit path with zero-alloc ResponseWriter |
Handler_CacheMiss_Cacheable | ~3.9 µs | 2311 | 13 (budget 18) | Cacheable miss (allocation-gated, not time-gated) |
SIEVE_Access | ~18 | 0 | 0 | SIEVE eviction policy access |
All hit-path benchmarks enforce 0 allocs/op in CI via benchmark gates. Any allocation on the hit path blocks merge.
Comparability note. The nightly load-test configuration enables
experimental.h1_fast_path(since v0.5.3) andexperimental.h1_reactor(since v0.5.5). Nightly proxy-comparison numbers from v0.5.2 and earlier were measured without the fast path and are not comparable.
Benchmark infrastructure
The benchmark suite lives in bench/ in the
bouine repository:
bench/
├── run.sh # Go micro-benchmark runner with gates
├── results/ # Baseline and current benchmark output
└── loadtest/
├── docker-compose.yaml # bouine + varnish + nginx + envoy + origin
├── config/ # Per-proxy configurations
├── scenarios/
│ ├── 3.2_hit_only/ # Pure cache-hit scenario
│ ├── 3.3_miss_storm/ # All-miss scenario
│ ├── 3.6_mixed_realistic/ # Production-like mixed workload
│ └── ... # Cluster, chaos, and edge-case scenarios
└── analysis/ # Python scripts for plotting and reportingTo reproduce the load-test results:
cd bench/loadtest
docker compose up -d origin bouine varnish nginx envoy
# Run a scenario
k6 run scenarios/3.2_hit_only/k6.js
# Generate a comparison report
python3 analysis/report.py results/To run the Go micro-benchmarks:
make bench # runs all benchmarks, checks gates, saves to bench/results/current.txt
make benchstat # compares current vs baseline with benchstat