You are reading documentation for bouine v0.4.x — not the latest version. View latest →

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.26. 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
bouinecurrent mainDefault 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.

Benchmarkns/opB/opallocs/opDescription
Evaluate_Hit3400RFC 9111 freshness evaluation
HotStore_Get_Hit5.200In-memory cache lookup (SIEVE)
BuildKey4800Cache key computation (xxhash64)
Handler_CacheHit78520328Full HTTP handler for a cache hit
Handler_CacheHit_ReusableWriter34600Hit path with zero-alloc ResponseWriter
HotStore_Put33915016Store a new object in the hot tier
SIEVE_Access5.200SIEVE eviction policy access

The Handler_CacheHit_ReusableWriter benchmark isolates the true hit-path cost by replacing httptest.NewRecorder with a reusable no-op ResponseWriter. The 8 allocs in Handler_CacheHit are from the httptest test harness, not from production code.

All hit-path benchmarks enforce 0 allocs/op in CI via benchmark gates. Any allocation on the hit path blocks merge.

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