Get a High-Performance WAF With Better Rules: Benchmarks on Rules vs. Engine
We benchmarked three Envoy WAF flavors. The engine overhead is minimal — the cost is in the rules you load — and full OWASP CRS body inspection is production-viable with the right build.
Every team that puts a Web Application Firewall in front of production traffic asks the same two questions: how much latency does it add, and how much hardware does it need? The answers floating around are usually anecdotes — measured on different proxies, different rule sets, and different payloads, so they can’t be compared or used for capacity planning.
We wanted real numbers, so we built a benchmark harness and measured three Envoy-based WAF flavors under identical conditions: same cluster, same pinned CPU cores, same traffic, same rule sets — including the full OWASP Core Rule Set (CRS). This post walks through the results and what they mean for sizing a WAF deployment. The full dataset is available for download at the end, so you can run your own analysis against your own traffic profile.
The headline findings:
- The WAF engine itself is close to free — the cost is in the rules you load. With a minimal policy (a handful of header rules plus SQLi/XSS detection), every flavor we tested held 10,000+ RPS on 6 cores1 while adding under 2.5 ms of p99 latency on bodies up to 10 KB — and the dynamic-module builds stayed under 1 ms. That’s the engine’s floor, not a typical deployment.
- Full OWASP CRS with request-body inspection is where implementations differentiate. The C-accelerated WAF that ships with Tetrate Enterprise Gateway for Envoy (TEG) sustained ~1,000 RPS per 6-core instance on 1 KB bodies with a 4.86 ms p992 — and it was the only flavor still sustaining that rate at 10 KB bodies.
- Rule-set scope is the biggest performance lever you have — bigger than the choice of implementation. Matching the rule set to the traffic matters more than any other tuning knob.
The three flavors we benchmarked
All three run the same battle-tested Envoy proxy; two of them add a WAF based on Coraza, the ModSecurity-compatible WAF engine that Tetrate engineers help maintain as members of the project:
- Vanilla Envoy — plain Envoy with no WAF filter. This is the baseline every other number is measured against.
- Pure Go Coraza WAF — Coraza compiled as a pure-Go Envoy dynamic module, with no C dependencies. That keeps it portable and dependency-free to distribute — you can run it on any stock Envoy today.
- Tetrate Enterprise Gateway for Envoy WAF (TEG WAF) — the same Coraza engine, built with C-accelerated matching: the RE2 regex engine and libinjection for SQLi/XSS detection. This is the enterprise-grade build that ships with TEG.
- Coraza Golang Envoy filter — the community coraza-envoy-go-filter, which embeds Coraza through Envoy’s Go HTTP filter instead of a dynamic module. Like TEG WAF, it links the C-accelerated matchers (RE2 and libinjection).
How we measured
Benchmarks are only useful if the harness can’t lie to you, so the environment was built to remove every source of noise:
- Dedicated, isolated hardware. A GKE cluster with a compute-optimized
c3-standard-8node (Sapphire Rapids) reserved exclusively for the system under test. Envoy ran as a single pod with 6 exclusively pinned vCPUs (Guaranteed QoS + static CPU manager), so no other workload could steal cycles. The load generator ran on a separate 22-vCPU node so it could never be the bottleneck, and all nodes shared one zone to keep network latency out of the measurement. - Coordinated-omission-safe load. Fortio drove constant target throughput across 64 connections with uniform request spacing, swept at 1,000 → 4,000 → 10,000 RPS to find each configuration’s knee.
- Benign traffic through the full pipeline. Requests are inspected but not blocked, so every request traverses the complete WAF pipeline and reaches the backend. We’re measuring processing cost, not early-exit shortcuts.
- Three rule-set tiers. Headers-only (phase-1 and phase-3 header rules, body access off), targeted rules (a typical custom policy: a handful of SecRules plus libinjection SQLi/XSS detection, with request body parsing on), and full OWASP CRS at default paranoia — the worst-case rule cost. The exact rules for each tier are listed below.
- Four traffic shapes. GET with headers only, then JSON POST bodies of 1 KB, 10 KB, and 50 KB.
The exact rule sets
All three tiers are plain SecLang — the ModSecurity-compatible rule language Coraza speaks — passed verbatim to each WAF flavor, so every implementation evaluates identical policy.
Headers-only keeps body access off, so only phase-1 header rules ever run — a URI block and a scanner User-Agent match. This is the cheapest possible WAF mode:
SecRuleEngine On
SecRequestBodyAccess Off
SecResponseBodyAccess Off
SecRule REQUEST_URI "@contains /admin" "id:101,phase:1,t:lowercase,log,deny,status:403"
SecRule REQUEST_HEADERS:User-Agent "@pm grabber masscan nikto sqlmap" "id:9131,phase:1,t:none,log,deny,status:403" Targeted rules (named simple-rules in the dataset) turns request-body parsing on with a bounded
12.5 MB limit, adds two custom SecRules, and runs the @detectXSS / @detectSQLi operators — backed
by libinjection in the C-accelerated builds — against all request arguments:
SecRuleEngine On
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyLimitAction ProcessPartial
SecResponseBodyAccess Off
SecRule REQUEST_URI "@contains /admin" "id:101,phase:1,t:lowercase,log,deny,status:403"
SecRule ARGS:payload "@streq maliciouspayload" "id:102,phase:2,t:lowercase,log,deny,status:403"
SecRule ARGS_NAMES|ARGS "@detectXSS" "id:9411,phase:2,t:none,t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNulls,log,deny,status:403"
SecRule ARGS_NAMES|ARGS "@detectSQLi" "id:9421,phase:2,t:none,t:utf8toUnicode,t:urlDecodeUni,t:removeNulls,multiMatch,log,deny,status:403"
SecRule REQUEST_HEADERS:User-Agent "@pm grabber masscan nikto sqlmap" "id:9131,phase:1,t:none,log,deny,status:403" Full OWASP CRS loads the entire Core Rule Set at its default paranoia level, with the same body limits. The CRS rule files ship embedded in the WAF module (via coraza-coreruleset), so the whole policy is three includes:
Include @coraza.conf
SecRequestBodyLimit 13107200
SecRequestBodyLimitAction ProcessPartial
SecResponseBodyAccess Off
Include @crs-setup.conf
Include @owasp_crs/*.conf How to read the numbers:
- A configuration is sustained when Envoy delivered ≥95% of the offered load with under 1% errors and CPU headroom to spare.
- A ≥ value is a ceiling — the sweep topped out at 10,000 RPS without saturating, so the true maximum is higher.
- A ~ value is saturated — the best throughput Envoy could deliver at that point, reported with the latency measured there.
Every run in the dataset completed with a 0% error rate; saturation showed up as throughput and latency knees, never as failures.
Result 1: the WAF engine is nearly free — the cost is in the rules
With the targeted rule set — request body parsing on, custom SecRules, and libinjection SQLi/XSS detection — all three WAF flavors rode the load generator’s 10,000 RPS ceiling1 on 6 cores3. This tier is deliberately minimal (five rules), so what it isolates is the engine’s fixed cost: body buffering, request parsing, and rule-phase execution, independent of any particular policy. Vanilla Envoy’s p99 at that load is about 1 ms; here is what each WAF added on top:
The starred (*) bar deserves a closer read: the Coraza Golang Envoy filter’s +1.25 ms at 50 KB looks lower than TEG WAF’s +5 ms — and even lower than its own +2.42 ms at 10 KB — but it is not a like-for-like number.4 At equal load, TEG WAF and the Pure Go Coraza WAF are the flavors that held the 10,000 RPS ceiling on 50 KB bodies.
For header-only GETs and bodies up to 10 KB, the added p99 stays under 2.5 ms for every flavor — and for the two dynamic-module builds it stays under 1 ms. Even at 50 KB bodies, TEG WAF adds 5 ms at 10,000 RPS, and the Pure Go Coraza WAF adds 12.39 ms while still holding the same throughput ceiling. CPU cost is just as modest: 0.16–0.33 cores per 1,000 RPS on bodies up to 10 KB (rising to ~0.4–0.6 at 50 KB), with peak memory under 80 MB in every case.
This is worth pausing on. A pure-Go WAF with zero native dependencies — one you can drop into any stock Envoy — inspects request bodies with injection detection at 10,000 RPS while adding less than a millisecond of tail latency on typical payloads.
To be clear about what this tier represents: five rules is not a WAF posture. It fits scenarios where you’re hunting for specific patterns — a scanner User-Agent, a known injection vector, one sensitive path — rather than deploying broad protection. What the result proves is that the engine’s fixed cost is negligible on every build, so the performance budget of a real deployment is spent almost entirely on the rules you choose to load. The next section measures the other end of that spectrum: the entire Core Rule Set.
Result 2: full OWASP CRS is where builds differentiate
The full Core Rule Set runs hundreds of rules against every request, and once request bodies are in play the cost profile changes completely. This is the workload where implementation engineering — which regex engine, how body buffering is handled, how the rule phases execute — decides the outcome:
On header-only traffic, all three flavors sustain ~3,900–4,000 RPS with the full CRS — and at moderate loads the tail latency is small (p99 of 2.5–3.6 ms at 1,000 RPS). The separation appears when bodies arrive:
| Payload | Pure Go Coraza WAF | TEG WAF | Coraza Golang Envoy filter |
|---|---|---|---|
| GET, headers only | 3,991 RPS · p99 26.74 ms | 3,978 RPS · p99 38.98 ms | 3,863 RPS · p99 44.63 ms |
| POST 1 KB | ~594 RPS (saturated5) | 991 RPS · p99 4.86 ms2 | 991 RPS · p99 6.70 ms |
| POST 10 KB | ~63 RPS (saturated5) | 964 RPS · p99 177.55 ms | ~976 RPS (saturated5) · p99 240.50 ms |
| POST 50 KB | not run6 | ~281 RPS (saturated) | ~263 RPS (saturated5) |
Three things stand out:
- TEG WAF is the only flavor that keeps sustaining ~1,000 RPS per instance as bodies grow to 10 KB. At 1 KB it pairs that throughput with a 4.86 ms p992 — full CRS inspection at single-digit-millisecond tail latency. RE2 and libinjection are doing exactly the job they were linked in for.
- The Coraza Golang Envoy filter tracks TEG WAF closely at 1 KB (991 RPS, 6.70 ms p99). It links the same C-accelerated matchers as TEG WAF (RE2 and libinjection), yet it saturates one step earlier as payloads grow and carries a slightly higher fixed per-request cost, so TEG WAF still comes out ahead: the Envoy dynamic-module data path is the leaner integration.
- The pure-Go build saturates early on this workload — by design, it’s built for a different job. Trading the C libraries away buys portability and zero-dependency distribution, which is the right trade for the scoped rule sets it excels at in Result 1. For full CRS on body-bearing traffic, reach for the C-accelerated build instead.
Full CRS also has a real resource footprint everywhere: peak memory grows from under 80 MB under targeted rules to 100–205 MB, and CPU cost at the knee ranges from 1.28 cores per 1,000 RPS at 1 KB bodies to 18.15 at 50 KB. These aren’t small numbers — they’re the honest price of running hundreds of rules against every request body, and they’re exactly what you need for capacity planning.
Sizing guidance you can take away
The numbers above are per single Envoy instance on 6 pinned cores; Envoy’s shared-nothing worker model means capacity scales out with replicas. From the measured data:
- Targeted rules: budget ~0.2–0.6 cores per 1,000 RPS depending on payload size, and well under 100 MB of memory per instance. Latency impact is inside the noise of most services.
- Full CRS on small bodies (≈1 KB): budget ~1.3 cores per 1,000 RPS on TEG WAF and expect a ~5 ms p99. A 6-core instance comfortably serves ~1,000 RPS.
- Full CRS on larger bodies: cost grows steeply with payload size (~4 cores per 1,000 RPS at 10 KB). If your traffic carries large payloads, scope the rules that inspect bodies — or put the full CRS only on the routes that need it.
- The cheapest optimization is policy, not hardware. The gap between targeted rules and full CRS is orders of magnitude larger than the gap between WAF implementations. The two tiers we measured are the endpoints of that spectrum: a production CRS deployment tuned for the backend it protects — excluding rule groups that don’t apply to your stack, and applying body inspection only on the routes that need it — lands somewhere in between. Start from what each route actually needs to inspect.
Which flavor for which workload
| Your workload | Reach for |
|---|---|
| Header rules, targeted custom SecRules, SQLi/XSS detection | Any flavor — including the portable, dependency-free Pure Go Coraza WAF provided by Built On Envoy |
| Full OWASP CRS on header-dominant traffic | Any flavor sustains ~4,000 RPS per instance |
| Full OWASP CRS with request-body inspection at production throughput | TEG WAF — the C-accelerated build is what keeps this workload sustained |
| Enterprise support, FIPS-ready builds, and a supported upgrade path | TEG — using the FIPS-validated images |
Run your own analysis
The complete dataset behind this post is available as a CSV download:
summary.csv.
It contains one row per flavor ×
rule set × payload × offered load, with achieved throughput, the full latency distribution (avg,
p50, p90, p99, p99.9), added latency vs the no-WAF baseline, error rate, CPU cores (average and
peak), peak working-set memory, and CPU cost per 1,000 RPS. In the CSV, the flavors appear as
baseline (vanilla Envoy), pure-go (Pure Go Coraza WAF),
clibs (TEG WAF), and gofilter (Coraza Golang Envoy filter), and the
targeted rule set appears as simple-rules.
Footnotes
- 10K RPS ceiling. The load sweep topped out at 10,000 RPS without saturating; figures described as ”≥” or as riding the ceiling are lower bounds, not measured maxima.
- 991 RPS at 4.86 ms p99. Measured on TEG WAF (
clibsin the dataset) with full OWASP CRS on 1 KB POST bodies, at that flavor’s maximum sustained load on a single 6-core instance. - Single instance. All results characterize one Envoy pod with 6 exclusively pinned vCPUs; multi-replica scaling was not measured, though Envoy’s shared-nothing worker model scales out horizontally.
- Unequal load on the starred bar. Every latency in the targeted-rules chart is reported at that flavor’s maximum sustained load. On 50 KB bodies the Coraza Golang Envoy filter could not sustain the 10,000 RPS step, so its bar is measured at 3,991 RPS while the other bars carry ≥9,990 RPS — shorter queues and a thinner tail at ~2.5× less load.
- Saturated (~) throughput. When a configuration could not sustain the offered load, we report the best achieved throughput with latency measured at that knee.
- Payloads above 50 KB. Not exercised in this benchmark run; do not extrapolate the Pure Go saturation curve beyond 10 KB.
Conclusion
The question “what does a WAF cost?” turns out to have a precise answer — it just depends on the rule set far more than on anything else:
- The WAF engine adds almost nothing on its own: with a minimal targeted policy, every flavor held 10,000+ RPS on 6 cores with single-digit-millisecond added p99 — under 1 ms on typical payloads for the dynamic-module builds, including the portable Pure Go Coraza WAF. Your performance budget is spent on the rules, so size the rule set to the backend you’re protecting.
- Full OWASP CRS with body inspection is a heavyweight workload, and the C-accelerated TEG WAF is what makes it sustainable at production throughput — ~1,000 RPS per 6-core instance with a 4.86 ms p99 at 1 KB bodies2, still sustained at 10 KB.
- All of this runs on the same Envoy data plane you already operate, with the same Coraza engine Tetrate engineers help maintain upstream.
Tetrate Enterprise Gateway for Envoy packages the C-accelerated WAF together with enterprise support and FedRAMP-ready FIPS builds of Envoy Gateway. If you’re planning a WAF rollout and want help mapping these numbers to your traffic, talk to us — or grab the dataset and start with your own numbers.
This is the first in a blog series about WAF on Envoy Gateway. In our next blog we will share more complete
benchmarks and details about per-request WAF cost.
Stay tuned!