Skip to content
Firewall latency benchmark

Request-path p95: 3.665msreproducible, on real hardware.

Published numbers, reproducible methodology, no marketing claims without measurements behind them. Where we haven't measured something, the table says so.

Headline result

3.67 ms p95 across 20,000 runs— on the REQUEST path, single-threaded, on real hardware, over prompts of 16–63 bytes. That is the number the 50ms SLA covers. Scan cost scales with payload, so this figure describes short prompts, not long ones — and the response-side scan is a second call with its own cost, published below by size rather than folded in here.

Not one run — the median of 5. We measure in independent blocks of 4,000 and publish the middle one whole, so every figure on this page comes from a single internally consistent run. The five blocks read 3.43 · 3.62 · 3.67 · 4.20 · 4.35 ms p95 (spread ×1.268, and the benchmark refuses to publish past ×1.3). Showing the spread is the point: our measurement machine is a shared workstation, not quiesced reference hardware, and a figure quoted without its distribution would be describing the machine rather than the firewall.

p50
2.44 ms
p95
3.67 ms
SLA target 50ms
p99
4.55 ms
max
23.18 ms
worst single request
mean
2.57 ms
throughput
388 req/s
single thread
total runs
20,000

Where the time goes

Per-layer breakdown — request path

Every layer that ran during the 20,000 request-path scans above, with the number of samples each figure came from. A layer this path does not exercise says not measured — it does not get a zero.

Layer
What it does
n
p50
p95
p99
pattern
Regex-based prompt injection / DAN / jailbreak / system-override patterns. Compiled at boot, no per-call overhead.
4,000
0.29
0.51
0.63
signature
Known-attack signature matching against the shipped corpus. Runs on every input scan — it was collected by the harness but discarded before publication until 2026-08-10, because the per-layer table was a fixed four-key object that had no slot for it.
4,000
0.05
0.11
0.15
token
DLP token scanning — 439 patterns covering SSN, credit card, AWS keys, API keys, etc. Linear scan, short-circuited on first hit.
4,000
0.03
0.04
0.06
semantic
Semantic similarity scoring against an embedding-based attack corpus. Dominates the input-path budget — single ML inference.
4,000
1.84
2.86
3.45
output
Leak / PII / harmful-content scan of the model's response. Runs only in scanOutput(), after the model has answered — so it contributes nothing to the request-path numbers above and is measured separately below.Not exercised on the input path — the output layer runs only in engine.scanOutput(), after the model has responded. Measured separately and size-bucketed under `outputPath`.
0
not measured
not measured
not measured

The other half

Output-path scan, by response size

The response scan is a separate call made after the model answers, so it does not add to the request-path figures above — but it is not free, and its cost is dominated by how long the response is. One number cannot describe that, so we publish the curve. Each row is a fresh set of scans over synthetic model responses padded to that size.

The 50ms target is a REQUEST-path budget and the largest responses exceed it. At 96 KB (383.4 ms p95) the output scan alone costs more than the whole input-path SLA target. We are publishing that rather than quoting the small-payload figure, because a buyer sizing a budget for long generations needs the number that applies to them. Budget the response scan separately, or run it out-of-band.
Response
bytes
n
scan p50
scan p95
output layer p50 / p95
512 B
512
600
1.14
2.03
0.54 / 0.81
4 KB
4,096
400
12.10
16.52
2.01 / 2.77
96 KB
98,304
100
282.04
383.40
32.36 / 50.29

Why there is no 1 MB row. The engine refuses to scan a response longer than 100,000 characters and fails closed on it in microseconds, so a larger bucket would be measuring the rejection path, not the detection path. The largest size we can honestly report is 96 KB. "scan p50/p95" is the whole scanOutput() call — leak/PII detection plus a pattern pass over the response — while the last column isolates the output layer itself.

How we measured

Methodology

  • 10 sample prompts, 16–63 bytes (median 42) covering benign queries, prompt injection ("ignore all previous instructions"), DAN jailbreaks, PII (SSN), system-prompt-leak attempts, base64-encoded payloads, and translations. The corpus is fingerprinted in the artifact, so a run over a different mix is visibly a different measurement.
  • Payload size is part of the result. Scan cost on this engine rises with input length — our per-class runs put a ~1–4.4 KB prompt near 48 ms p95 against roughly 4 ms for a short one, on the same build. So the headline above describes short prompts, and the sizes it was measured over are published rather than left for the reader to assume.
  • JIT warm-up before measurement on both paths, so the V8 hot path doesn't poison the p50.
  • Single thread, no concurrency. Throughput under concurrency would be higher; we publish the conservative number.
  • 5 independent blocks, median published. Each block builds a fresh engine and runs 4,000 scans; the block whose p95 is the median is published in full, and every block's p95 ships in the artifact. The benchmark refuses to publish at all if the blocks disagree by more than ×1.3, because a procedure that will not reproduce itself has not measured anything. This replaced a single-run figure that was published and withdrawn on the same day: our box swings roughly ±40% with concurrent load, which is larger than any regression we would be trying to detect.
  • What the headline covers: the REQUEST path only pattern, signature, token, semantic. The semantic layer takes most of that budget. The output layer is a separate call after the model responds and is reported by response size in its own table; folding it into a single "all layers" figure is what let this page claim for 102 days that it was sub-millisecond, when it had never been run at all.
  • Unmeasured is never zero. Every figure published here carries the sample count it came from. Where a layer was not exercised, the table says not measured and the artifact stores null. A CI gate fails the build if a statistic is ever published from zero samples.
  • The benchmark runs the production artifact — this run timed packages/core/dist/firewall/detection-engine.js, the compiled bundle that ships as @evalguard/core, not the TypeScript source.

Run it yourself

Reproduce this on your hardware

Clone the public repo, run one command, get the same JSON shape.

# Public benchmark mirror — runs the published @evalguard/core engine
git clone https://github.com/EvalGuardAi/evalguard-benchmarks
cd evalguard-benchmarks
npm install

# Default run — 5,000 iterations, plain stdout
node benchmark-firewall-latency.mjs

# JSON output (same shape this page reads)
node benchmark-firewall-latency.mjs --runs=20000 --json > latency.json

# Or via the npm script
npm run bench

The mirror's own public CI re-runs this weekly and on every push, failing past 50ms p95 on the request path — see the runs.

Chain of custody

Provenance

Measured at
August 10, 2026 · 14:37:43 GMT UTC
Machine
Intel(R) Core(TM) i7-7820HK CPU @ 2.90GHz · 8 cores · 31.9 GB · Node v24.12.0
Commit
fbcf7ab37c55
Source
scripts/benchmark-firewall-latency.mjsengine: distpackages/core/dist/firewall/detection-engine.js
Corpus
prompts 10 · 71fd696ce5a8responses 24 · 87862ddbcb77
JSON
/trust/latency.jsonbyte-identical to the file this page renders; a CI gate fails if they diverge

Numbers are refreshed on every release that touches the firewall path. CI gates prevent regressions, block a statistic published from zero samples, and fail the build once this measurement passes its age limit.

Want full SLA + uptime guarantees?

The latency budget here is a code-level measurement. Per-tier SLA + uptime history lives on the SLA page.

View SLA