95% detection rate.
That’s the number your WAF vendor showed you in the demo. Here’s what it was measured against.
Level 1 - the bare bot
The requests/curl bot. No cookies. No Sec-Fetch-Dest, no Sec-Fetch-Mode. Accept-Language missing. Default or empty User-Agent. Identical TLS fingerprint across thousands of requests. Fixed timing between requests.
Your WAF blocks it in 3 requests. Of course - it’s like spotting someone walking into an airport without pants.
Level 2 - the automated browser
It has the headers, the cookies, a correct UA. But navigator.webdriver is true. 0 plugins. hardwareConcurrency at 1. window.chrome missing. The header order doesn’t match the TLS fingerprint - a Chrome JA3 with headers in Firefox’s order is a signal. Playwright or CDP frames leak in stack traces.
Your WAF blocks it. Still relatively trivial.
Level 3 - the bot that tries
webdriver patched. 3 PDF plugins. window.chrome stubbed. Headers in the right order. Cookies sent back. But the canvas hash changes between sessions. WebGL parameters don’t match the declared GPU. toString() on native functions reveals overrides. The prototype chain has injected getters that don’t exist on a real Chrome.
This is where the detection rate starts to drop.
Level 4 - the behavioral bot
It passes the JS checks. But the coefficient of variation in its mouse movements is too low. Its heatmap is a rectangle instead of an F. It scrolls at constant speed without ever scrolling back up. Its cursor is perfectly static during pauses. Zero text selection. Zero tab switching. It crawls at 3 AM at the same pace as 10 AM.
This is where even good WAFs start to give up.
Level 5 - the bot that reproduces human distributions
Log-normal timing between movements. F-pattern heatmap with the right spatial entropy. Scrolling with inertia and micro-backscrolls. Gaussian tremor during pauses. Text selection on 12% of pages. Canvas seeded and persisted cross-session. toString masked all the way to stack traces. 60+ corrected signals.
We built this tool. It passes Akamai Content Protector.
At this level, most WAFs give up. Not all. DataDome still blocks it - which means they’re exploiting signals we haven’t identified yet. It’s a work in progress.
What actually holds
What we know is that the signals that hold the longest are those that live outside the browser:
- The HTML/assets ratio in server logs - the bot only loads HTML because loading all resources costs x25 in bandwidth.
- System font metrics -
measureText()depends on the OS rendering engine, not the browser. A headless Linux doesn’t draw glyphs the same way as Windows. - The TCP/TLS fingerprint from the kernel - managed by the operating system, not by JavaScript.
Not JavaScript. Not behavioral alone. Physics and economics.
The 95% figure measures level 1. The real question isn’t “does your WAF detect bots”.. it’s at what level it gives up.
Tested against which level, your 95%?
