Light WAF against no WAF and against the reference open-source WAF, ModSecurity v3 with the OWASP Core Rule Set — same machine, same CPUs, pinned images. One script reproduces every number on this page, and the caveats are part of the result.
Run of 2026-10-28 on an AMD Ryzen 7 5800X 8-Core Processor, Docker 29.9.2 linux/amd64. Each WAF is pinned to the same 2 CPUs; the backend and the load generator run on other cores. Full setup below.
Latency is measured at a fixed 250 requests per second, a load every target sustains; added p99 is the target's p99 minus the p99 without a WAF. Throughput is the most the target serves with 64 connections and no rate limit.
A browser-like GET with a query string and eight headers, a cookie among them.
| Target | p50 | p99 | p99.9 | added p99 | max req/s | vs no WAF |
|---|---|---|---|---|---|---|
| No WAF | 0.13 ms | 0.27 ms | 0.42 ms | – | 50,397 | – |
| Light WAF · PL2, threshold 5 | 0.35 ms | 0.56 ms | 0.90 ms | +0.30 ms | 14,055 | 27.9% |
| Light WAF strict · PL4, threshold 3 | 0.36 ms | 0.66 ms | 1.06 ms | +0.40 ms | 13,656 | 27.1% |
| ModSecurity + CRS · PL1, threshold 5 | 1.76 ms | 2.82 ms | 4.01 ms | +2.55 ms | 1,120 | 2.2% |
| ModSecurity + CRS · PL2, threshold 5 | 2.40 ms | 3.81 ms | 14.9 ms | +3.54 ms | 800 | 1.6% |
The same headers plus a ~400-byte JSON order body.
| Target | p50 | p99 | p99.9 | added p99 | max req/s | vs no WAF |
|---|---|---|---|---|---|---|
| No WAF | 0.13 ms | 0.23 ms | 0.50 ms | – | 50,022 | – |
| Light WAF · PL2, threshold 5 | 0.40 ms | 0.62 ms | 0.99 ms | +0.39 ms | 12,332 | 24.7% |
| Light WAF strict · PL4, threshold 3 | 0.41 ms | 0.69 ms | 1.04 ms | +0.46 ms | 12,153 | 24.3% |
| ModSecurity + CRS · PL1, threshold 5 | 2.82 ms | 7.43 ms | 16.8 ms | +7.19 ms | 644 | 1.3% |
| ModSecurity + CRS · PL2, threshold 5 | 4.50 ms | 13.1 ms | 25.6 ms | +12.8 ms | 440 | 0.9% |
A GET carrying a SQL injection. Every WAF blocks it, so this is the cost of a deny.
| Target | p50 | p99 | p99.9 | added p99 | max req/s | vs no WAF |
|---|---|---|---|---|---|---|
| No WAF | 0.13 ms | 0.24 ms | 0.44 ms | – | 51,131 | – |
| Light WAF · PL2, threshold 5 | 0.22 ms | 0.45 ms | 0.77 ms | +0.21 ms | 20,397 | 39.9% |
| Light WAF strict · PL4, threshold 3 | 0.22 ms | 0.43 ms | 0.70 ms | +0.19 ms | 20,235 | 39.6% |
| ModSecurity + CRS · PL1, threshold 5 | 1.45 ms | 2.32 ms | 3.95 ms | +2.08 ms | 1,305 | 2.6% |
| ModSecurity + CRS · PL2, threshold 5 | 2.12 ms | 4.31 ms | 7.88 ms | +4.07 ms | 857 | 1.7% |
Memory is each WAF container's usage after its load runs:Light WAF · PL2, threshold 5 20.77MiB, Light WAF strict · PL4, threshold 3 25.85MiB, ModSecurity + CRS · PL1, threshold 5 102.8MiB, ModSecurity + CRS · PL2, threshold 5 103.1MiB.
Measured with gotestwaf: its attack sets (SQLi, XSS, traversal, RCE, SSTI, XXE, LDAP, NoSQL, mail and SSI injection, CRLF, …, each through several encodings and placements) and its set of legitimate requests that look suspicious. A403 or a 400 counts as blocked for every WAF.
| Target | Attacks blocked | Legitimate passed | gotestwaf score |
|---|---|---|---|
| Light WAF · PL2, threshold 5 | 76.4% (516/675) | 100.0% (141/141) | 65.7% |
| Light WAF strict · PL4, threshold 3 | 96.0% (648/675) | 100.0% (141/141) | 99.0% |
| ModSecurity + CRS · PL1, threshold 5 | 49.2% (332/675) | 90.8% (128/141) | 63.5% |
| ModSecurity + CRS · PL2, threshold 5 | 54.7% (369/675) | 64.5% (91/141) | 65.4% |
| ModSecurity + CRS strict · PL4, threshold 3 | 75.4% (509/675) | 0.0% (0/141) | 68.7% |
gotestwaf's overall score averages application and API security; its GraphQL and gRPC sets are not run here (no such backend), so the API part rests on a handful of REST and SOAP cases.
| Test case | Light WAF · PL2, threshold 5 | Light WAF strict · PL4, threshold 3 | ModSecurity + CRS · PL1, threshold 5 | ModSecurity + CRS · PL2, threshold 5 | ModSecurity + CRS strict · PL4, threshold 3 |
|---|---|---|---|---|---|
| community/community-128kb-rce | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-128kb-sqli | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-128kb-xss | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-16kb-rce | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-16kb-sqli | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-16kb-xss | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-32kb-rce | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-32kb-sqli | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-32kb-xss | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-64kb-rce | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-64kb-sqli | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-64kb-xss | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-8kb-rce | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-8kb-sqli | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-8kb-xss | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| community/community-lfi | 8/8 | 8/8 | 8/8 | 8/8 | 8/8 |
| community/community-lfi-multipart | 0/2 | 0/2 | 2/2 | 2/2 | 2/2 |
| community/community-rce | 4/4 | 4/4 | 2/4 | 4/4 | 4/4 |
| community/community-rce-rawrequests | 2/3 | 3/3 | 3/3 | 3/3 | 3/3 |
| community/community-sqli | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 |
| community/community-user-agent | 9/9 | 9/9 | 6/9 | 6/9 | 6/9 |
| community/community-xss | 82/104 | 99/104 | 98/104 | 98/104 | 104/104 |
| community/community-xxe | 2/2 | 2/2 | 0/2 | 0/2 | 2/2 |
| owasp-api/non-crud | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 |
| owasp-api/rest | 3/7 | 7/7 | 4/7 | 5/7 | 7/7 |
| owasp-api/soap | 1/5 | 5/5 | 2/5 | 3/5 | 5/5 |
| owasp/crlf | 6/7 | 6/7 | 6/7 | 6/7 | 7/7 |
| owasp/ldap-injection | 22/24 | 22/24 | 2/24 | 4/24 | 18/24 |
| owasp/mail-injection | 20/24 | 20/24 | 3/24 | 6/24 | 15/24 |
| owasp/nosql-injection | 20/50 | 50/50 | 12/50 | 21/50 | 33/50 |
| owasp/path-traversal | 20/20 | 20/20 | 6/20 | 8/20 | 14/20 |
| owasp/rce | 6/6 | 6/6 | 2/6 | 3/6 | 5/6 |
| owasp/rce-urlparam | 9/9 | 9/9 | 3/9 | 9/9 | 9/9 |
| owasp/rce-urlpath | 3/3 | 3/3 | 1/3 | 2/3 | 2/3 |
| owasp/shell-injection | 24/32 | 32/32 | 6/32 | 10/32 | 24/32 |
| owasp/sql-injection | 45/48 | 45/48 | 20/48 | 20/48 | 32/48 |
| owasp/ss-include | 24/24 | 24/24 | 12/24 | 12/24 | 15/24 |
| owasp/sst-injection | 24/24 | 24/24 | 11/24 | 12/24 | 15/24 |
| owasp/xml-injection | 7/7 | 7/7 | 3/7 | 3/7 | 7/7 |
| owasp/xss-scripting | 146/224 | 214/224 | 91/224 | 95/224 | 143/224 |
Its detection modules were developed by running gotestwaf and closing the bypasses it found; the CRS was not. That favours Light WAF by construction — read its gotestwaf score as a regression measure more than an independent ranking. An independent dataset is on the to-do list.
PL2 in Light WAF is not PL2 in the CRS: the two rule sets grade their rules differently. Several CRS levels are shown, and both WAFs run their documented settings with no rule exclusions.
gotestwaf's legitimate set is small (141 requests), so a clean score there is not zero false positives on real traffic. Paranoia 4 with threshold 3 blocks on accumulated weak signals: run it in detection-only mode on your traffic first. The strict CRS, with no exclusions, blocks even the clean benchmark requests, so it appears in the detection results only.
The numbers are those of ModSecurity v3 — libmodsecurity 3.0.17 with the nginx connector 1.0.4 and CRS 4.30.0, from the official owasp/modsecurity-crs image — not of ModSecurity in general. ModSecurity v2 on Apache, or the CRS on another engine, will perform differently.
Light WAF does not inspect responses, so ModSecurity's response-body inspection is off. Both keep their shipped logging. Nothing is tuned to win.
Absolute numbers depend on the CPU, the CPU sets and on Docker running in a VM or natively. Compare targets within one run. This is a WAF's own cost on a loopback network — no TLS, no real application.
Corrections welcome. Maintain one of the compared projects, or think the setup is unfair to it? Open an issue: we'll fix the setup, re-run, and say on this page what changed.
Reproduce it with cd bench && ./run.sh — Docker and bash are the only requirements. The methodology describes every step.
| Host | AMD Ryzen 7 5800X 8-Core Processor · 16 CPUs visible to Docker · Docker Desktop · kernel 6.18.54.1-microsoft-standard-WSL2 |
| CPU sets | WAF 0-1 · backend 2 · load generator 3-5 |
| Load | 30s per run after a 5s warm-up · 64 connections · fixed-rate runs at 250 req/s with coordinated-omission correction |
| backend | nginx:1.27.5-alpine 6769dc3a703c |
| lightwaf | ghcr.io/0x00spor3/waf-enterprise:1.0.1 0d1e085d70e9 |
| lightwaf-strict | ghcr.io/0x00spor3/waf-enterprise:1.0.1 0d1e085d70e9 |
| modsec-pl1 | owasp/modsecurity-crs:4.30.0-nginx-202610051210 c771ea3ed896 |
| modsec-pl2 | owasp/modsecurity-crs:4.30.0-nginx-202610051210 c771ea3ed896 |
| modsec-strict | owasp/modsecurity-crs:4.30.0-nginx-202610051210 c771ea3ed896 |
| loadgen | ghcr.io/hatoo/oha:1.16.0 829061114b87 |
| gotestwaf | wallarm/gotestwaf:0.5.8 1f8556919f24 |