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

ProxyVersionConfiguration
bouinev0.5.x (current main)Default config with stale_while_revalidate: 30s
Varnish7.xDefault VCL with grace period, SWR enabled
NGINX1.25.xproxy_cache with default settings
Envoy1.29.xHTTP cache filter with default settings

Scenarios

ScenarioDescriptionRPSVUsHit rate target
3.2 Hit-onlyWarm cache, all requests are cache hits3000200100%
3.3 Miss stormCache-Control: no-store on all responses15001000%
3.6 Mixed realistic60% cacheable + 40% non-cacheable + revalidation3000300~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.

ProxyAvg latencyp90p95RPS achieved
bouine0.166 ms0.219 ms0.277 ms2996
Varnish0.177 ms0.201 ms0.263 ms2999
NGINX0.166 ms0.304 ms3000
Envoy0.232 ms0.300 ms2999

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

ProxyAvg latencyp90p95RPS achieved
bouine0.157 ms0.186 ms0.232 ms1500
Varnish0.166 ms0.218 ms0.298 ms1500
NGINX541 ms (overloaded)1501 ms3002 ms1283
Envoy0.307 ms0.172 ms0.189 ms1500

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.

ProxyAvg latencyp90p95Hit rate
bouine0.278 ms0.258 ms0.334 ms72.8%
Varnish0.233 ms0.260 ms0.385 ms93.0%
NGINX22.1 ms (overloaded)0.625 ms72.9%
Envoy0.281 ms0.273 ms0.354 ms0% (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

Scenariobouine (original)bouine (optimized)Improvementvs Varnish
Hit-only0.338 ms0.166 ms-51%2.1x slower → 6% faster
Miss storm0.647 ms0.157 ms-76%4.4x slower → 5% faster
Mixed0.485 ms0.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).

Benchmarkns/opB/opallocs/opDescription
Evaluate_Hit~4500RFC 9111 freshness evaluation
HotStore_Get_Hit~1800In-memory cache lookup (SIEVE)
BuildKey~4800Cache key computation (xxhash64)
FastPath_Hit~12900Full H1 fast-path cache hit (parse + lookup + writev)
H1Parse_Get~19200H1 request parse (h1parser)
Handler_CacheHit_ReusableWriter~39000Hit path with zero-alloc ResponseWriter
Handler_CacheMiss_Cacheable~3.9 µs231113 (budget 18)Cacheable miss (allocation-gated, not time-gated)
SIEVE_Access~1800SIEVE 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) and experimental.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 reporting

To 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