Skip to content

Benchmark cookie cost and refresh the per-request numbers - #6

Merged
ndreno merged 2 commits into
mainfrom
docs/refresh-cost-numbers
Sep 11, 2026
Merged

ndreno merged 2 commits into
mainfrom
docs/refresh-cost-numbers

Conversation

@ndreno

@ndreno ndreno commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Why

Answering "do we need to redo the benchmark and update the docs?" after the selector-precompile (#3) and copy-avoidance (#4) changes. Yes to both, and the benchmark had a blind spot worth fixing.

The blind spot

bench_inspect measured cookie-less requests only. But CRS resolves REQUEST_COOKIES on 162 rules, each with a !REQUEST_COOKIES:/__utm/ exclusion, and before selectors were compiled once that recompiled a regex per cookie, per rule, per request. So the one path that dominated real (browser) traffic was the one the benchmark never touched. The README's "1.9 ms in a real gateway vs ~0.3 ms isolated" gap was precisely this: live requests carry cookies, the benchmark didn't.

Same-machine A/B (added cookie cases, run against the pre-fix and current engine)

case before after speedup
GET, no query 313 µs 52 µs 6.0×
GET, short query 344 µs 82 µs 4.2×
GET, 10 params 596 µs 269 µs 2.2×
POST, form 385 µs 113 µs 3.4×
POST, 4 KB JSON 758 µs 472 µs 1.6×
GET, browser cookies (9) 1598 µs 277 µs 5.8×
GET, 24 cookies 3681 µs 604 µs 6.1×
attack, sqli 354 µs 83 µs 4.3×

The pre-fix "before" GET-no-query (313 µs) matches the README's old 311 µs, confirming the same machine. Two distinct wins: cookie-free requests got 1.6–6× from testing values in place instead of copying every one (#4, pure allocation churn on requests where regexes mostly reject); cookie requests got 5.8–6.1× from that plus compiling selectors once (#3). The 3.7 ms pre-fix 24-cookie case confirms the microbenchmark that motivated #3.

Changes

  • Two cookie cases in the benchmark (a typical browser jar with __utm*/_pk_ref, and a 24-cookie jar).
  • Wire bench_inspect as a [[bin]], which its own header documented (cargo run --bin bench_inspect) but the manifest never declared.
  • Refresh the README table with current numbers, and note cookies are a first-order cost.
  • Drop the stale in-gateway figure, the "500 req/s", and the "7% copy attempt" line: the in-gateway number was measured against the old engine, and per the docs convention a deployment should measure its own.

Not touched: barbacane's docs

barbacane pins an old parapet rev, so its shipped gateway still has the slow path and its docs/guide/waf.md "~2 ms per request" is still accurate for what it ships today. Updating it before bumping the parapet rev would claim a speed the binary doesn't have. The correct sequence there is: bump the rev, re-measure in-gateway, then update the barbacane doc, as a separate change.

The cost benchmark measured cookie-less requests only, so it never exercised
the path that dominated real traffic: CRS resolves REQUEST_COOKIES on 162 rules
with a `!REQUEST_COOKIES:/__utm/` exclusion, and before selectors were compiled
once that recompiled a regex per cookie per rule per request. A browser request
with nine cookies cost 1.6 ms; twenty-four cost 3.7 ms. The published table,
all cookie-free, showed none of it.

Adds two cookie cases (a typical browser jar, and a heavy one) and wires
bench_inspect as a binary, which its own header already documented but the
manifest never declared.

Refreshes the README table with current numbers on the same machine. Selectors
are now compiled once and inspected values are tested in place rather than
copied, so cookie-free requests are several times cheaper and cookie-bearing
requests no longer scale with a per-cookie recompile:

    GET, no query        313 -> 52 us
    GET, browser cookies 1598 -> 277 us
    GET, 24 cookies      3681 -> 604 us

Drops the stale in-gateway figure and the "500 req/s" and "7% copy attempt"
lines: the in-gateway number was measured against the old engine and a
deployment should measure its own.
A second [[bin]] made cargo run -p parapet-conformance ambiguous, and the
conformance CI jobs invoke it without --bin, so they failed to run (the test
job only builds, so it stayed green). An [[example]] keeps the package's single
default binary and leaves those jobs untouched.
@ndreno
ndreno merged commit 5e6f08a into main Sep 11, 2026
9 checks passed
@ndreno
ndreno deleted the docs/refresh-cost-numbers branch September 11, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant