Skip to content

First performance benchmark

Barbacana is an HTTP reverse proxy that runs the OWASP CRS v4 ruleset on every request. This blog entry explains how it was benchmarked on two Google Cloud instance types — c3-standard-4 (4 vCPU) and e2-standard-8 (8 vCPU) — across six load tiers from 100 to 1500 requests per second (RPS), using a mixed workload of GET, POST, file uploads, and simulated attack traffic.

Per-vCPU throughput was consistent across both machines at approximately 125 RPS per vCPU. p99 latency stayed between 35 and 65 ms across the operating range, and memory remained between 119 and 137 MB until saturation. All simulated attack requests were blocked at every load level.

CPU profiling confirmed the epxected outcome: the dominant cost is CRS rule evaluation; the proxy layer itself adds no measurable overhead. This post describes the methodology, results, and operational implications.

Setup

The benchmark ran on two GCP instance types. Three binaries ran on the same machine in each run: Barbacana (the WAF), k6 (the load generator), and Caddy (a mock backend returning 200 OK for every route).

Machine vCPU RAM Instance class
c3-standard-4 4 16 GB compute-optimised
e2-standard-8 8 32 GB general-purpose

Barbacana ran with its default configuration — the same setup any user gets out of the box, with most protections automatically enabled.

Measuring on a single machine is appropriate because the primary metric — waf_request_duration_overhead_seconds — is a histogram recorded inside Barbacana, covering only the WAF's own processing time. Network topology between the load generator and the WAF does not affect it. The mock backend adds no meaningful latency (same host, loopback), so end-to-end latency in this test approximates WAF overhead directly.

Mixed workload

Each k6 iteration picks one request type at random:

Type Weight Details
GET page 79.5% Random path from a slug list; optional query string
GET static asset 10% Random path under /assets/ — css, js, images
POST JSON 5% Content-Type: application/json, random 1–5 KB body
POST form 4% Content-Type: application/x-www-form-urlencoded, login fields
File upload 1% Content-Type: multipart/form-data, random 100 KB–1 MB payload
Attack traffic 0.5% SQL injection, XSS, path traversal, command injection

The mix reflects the request shapes a Barbacana instance would see in a typical production deployment: mostly page and asset fetches, some API and form traffic, occasional large uploads, and a small fraction of attack attempts. Inspection work varies by content type — the workload exercises Barbacana across the full range of request shapes it would encounter in practice.

Methodology

Each tier ran at a constant target rate for the full measurement window — not a ramp, not a burst, but a steady sustained load held at exactly the specified RPS for 10 minutes. 60 seconds of warmup traffic preceded each measurement window and was discarded. Prometheus metrics were scraped from Barbacana's /metrics endpoint every 5 seconds. CPU and memory were read directly from Linux metrics at /proc folder during the same window.

Results

c3-standard-4 (4 vCPU, compute-optimised)

RPS p99 RAM (Barbacana) CPU (Barbacana)
100 36.6 ms 120 MB 335 m
500 59.1 ms 125 MB 2060 m
1000 > 500 ms (collapsed) 350 MB 3500 m

e2-standard-8 (8 vCPU, general-purpose)

RPS p99 RAM (Barbacana) CPU (Barbacana)
100 40.0 ms 123 MB 534 m
250 38.9 ms 119 MB 880 m
500 41.8 ms 125 MB 1872 m
750 48.4 ms 133 MB 3427 m
1000 64.9 ms 137 MB 4508 m
1500 1976 ms (collapsed) 360 MB 6931 m

The p99 (99th percentile) is the latency threshold that 99% of requests fell below. It measures tail behaviour rather than the average — if p99 is acceptable, almost every user sees acceptable latency, including during brief traffic spikes that push the tail higher. Latency is computed from the waf_request_duration_overhead_seconds histogram. CPU is in Kubernetes millicores (1000m = one full core-second per second).

Per-vCPU scaling

Both runs converge on the same throughput per core:

  • c3-standard-4: 500 RPS sustained on 4 vCPU → 125 RPS per vCPU
  • e2-standard-8: 1000 RPS sustained on 8 vCPU → 125 RPS per vCPU

Different CPU families, different clock speeds, different instance classes — the same per-vCPU result. This is the number that transfers across hardware: divide your target RPS by 125 to get the vCPU count you need.

The e2 data shows p99 degrading smoothly from 40.0 ms at 100 RPS to 64.9 ms at 1000 RPS, then collapsing at 1500 RPS. There is no sudden cliff in the operating range — headroom above your target RPS translates directly into lower p99.

Saturation

Every system has a load point where it can no longer keep up with incoming requests. Requests start queuing faster than they complete, latency climbs, and throughput falls below the target rate. Knowing where this threshold sits lets you size instances with enough margin to stay clear of it in production.

On the e2-standard-8, 1500 RPS caused saturation: the load generator could not initialise virtual users fast enough to sustain the target rate, average latency climbed above 1.9 seconds, and Barbacana consumed nearly all 8 vCPU (6931m out of 8000m). The c3-standard-4 hit the same per-vCPU wall at 1000 RPS — 3500m of 4000m consumed, ~33% of iterations dropped.

This is per-instance saturation under sustained load, not a machine ceiling. For traffic that sustains above the ~125 RPS per vCPU threshold, add instances behind a load balancer. The data plane is stateless — no coordination between instances is required. The per-instance ceiling stays the same; total throughput scales linearly.

Memory

Memory across the full operating range (100–1000 RPS) stayed between 119 MB and 137 MB on both machines. This is validated across two hardware types: the baseline footprint is dominated by CRS rule loading, and per-request allocations are small. Memory only grows under saturation — 350 MB on the c3 and 360 MB on the e2 — where requests queue faster than they complete.

Attack blocking

All ~3,900 simulated attack requests returned 403 across all tiers on both machines, including tiers where Barbacana was running at the edge of its capacity. A WAF that slows under load but never stops protecting is exactly the right behaviour for DDoS scenarios: sustained high traffic may push latency higher, but attack requests are still identified and blocked throughout. This protection guarantee held without exception across every tier in the benchmark.

k6 reports an http_req_failed rate of approximately 1.2%. This is a k6 measurement artifact: k6 counts any non-2xx response as a failure by default, which includes the 403 responses for blocked attacks. Separate check metrics in k6 confirmed that every request — including blocked attacks — behaved as expected.

Profiling

CPU profiling records which functions consume processor time during a live run. It shows where the process actually spends its cycles, rather than where developers assumed it would. Go's built-in pprof tool was used to capture a 60-second profile at steady state on the e2-standard-8 at 250 RPS. Three findings:

CRS rule evaluation accounts for 75% of cumulative CPU. corazawaf.(*Rule).doEvaluate sits at 4.10% flat but 75.14% cumulative — it is the call that drives all downstream work. That work is regex matching, distributed across regexp.(*machine).add (12.90% flat), .step (5.03%), .tryBacktrack (4.90%), and regexp/syntax.(*Inst).MatchRunePos (2.57%). No single rule or rule group dominates; the load is spread proportionally across the CRS ruleset.

No Barbacana-specific application function exceeds 5% of flat CPU. The entire Caddy/Barbacana proxy stack — connection handling, routing, middleware, instrumented response writing — appears at 0.00–0.06% flat while accounting for 84–88% cumulative, meaning it orchestrates work but does none of its own. The proxy layer adds no measurable overhead beyond CRS rule evaluation.

For the full CPU profile breakdown, see the detailed profile analysis. To inspect the profile yourself, download the raw pprof file and run go tool pprof v060-cpu_250.pprof.

Limitations

Two machine types tested, both Google Cloud. Results may differ on other cloud providers, bare-metal servers, or ARM instances. The per-vCPU finding is consistent across both tested types but has not been validated more broadly.

Workload mix. The mix reflects typical web traffic patterns. Workloads with unusually high upload ratios, large POST bodies, or heavy API traffic will shift the per-vCPU ceiling.

p99 variation. Measured on quiet, dedicated instances. On shared or noisy infrastructure, expect run-to-run variation, particularly at p99.

k6 http_req_failed rate. The ~1.2% figure counts 403 responses as failures. All requests completed with the expected status codes, confirmed by the separate k6 check metrics.

Conclusion

Barbacana delivers ~125 RPS per vCPU with full CRS protection, p99 latency between 35 and 65 ms across the operating range, a stable ~130 MB memory footprint, and uninterrupted attack blocking under sustained load. Scale by adding vCPUs to a single instance or by adding replicas behind a load balancer — both behave linearly. For resource planning and Kubernetes manifests, see the operations sizing guide.


AI assistance was used to structure and draft this post; the data and final text were reviewed by a human.

The benchmark relied on k6 to generate load and validate blocking rates. k6 is an open-source load testing tool that made it straightforward to build a realistic mixed workload and collect per-request outcome data. Thanks to the k6 team and contributors.