Browser automation is not inherently malicious. It powers integration tests, uptime monitors, and accessibility tools. But it is also the engine behind credential stuffing, scraping, scalping, and fake-account creation, and when it shows up at your login or checkout it is almost never friendly. The job of browser automation detection is to separate a real person driving a browser from a script pretending to be one.
The good news is that automation frameworks leak. Selenium, Puppeteer, and Playwright each inject hooks, alter the runtime, and produce behavior no human generates. The bad news is that every well-known leak has a well-known patch, so no single check survives contact with a motivated attacker. The durable approach is to collect many weak, independent signals and score them together, so that closing one leak opens another.
The obvious flags, and why they are only a start
The first generation of tells are the ones every tutorial mentions. They still catch a surprising volume of low-effort bots, but you should assume any competent operator has neutralized them.
navigator.webdriveris set totrueby the W3C WebDriver spec whenever a browser is under automation. Unpatched Selenium reveals itself instantly here.- Chrome under Puppeteer historically exposed a
window.chromeobject that differed subtly from a real Chrome, or omitted it entirely in some configurations. - The default
User-Agentfor headless Chrome once contained the literal stringHeadlessChrome, and old PhantomJS builds advertised themselves outright. - Automation-injected globals such as
cdc_prefixed properties from ChromeDriver, or__playwrightand__puppeteerevaluation artifacts, sometimes linger in the page context.
Treat any of these firing as strong positive evidence. Treat their absence as nothing at all, because patching them is the first thing a stealth setup does. This asymmetry, where a signal only counts when present, runs through the entire discipline.
Environment inconsistencies
Automation frameworks run in environments that differ from consumer machines in ways that are tedious to fully disguise. The most productive checks look for internal contradictions rather than any single value.
- Headless renderers historically reported zero
navigator.pluginsand emptynavigator.mimeTypes, where a real desktop browser reports a small consistent set. - The permissions API can contradict itself. A classic tell is querying notification permission and finding it reported as
deniedwhileNotification.permissionreadsdefault, a state a genuine browser does not produce. - WebGL vendor and renderer strings frequently reveal
SwiftShader,llvmpipe, or a generic Mesa driver, betraying software rendering on a server with no real GPU. See our WebGL fingerprinting guide for how this surface is read. - Screen and viewport dimensions that never change across a fleet, or a
window.outerHeightof zero, point at a headless viewport rather than a windowed browser. - Font enumeration comes back thin. A headless Linux container ships a handful of fonts where a real Windows or macOS install carries dozens, a gap covered in font fingerprinting.
None of these is decisive alone. A privacy-hardened real browser might reduce plugins to zero. But a client that shows software rendering, a bare font list, and a null outer height simultaneously is not a hardened human, it is a data-center headless instance.
Timing and behavioral tells
The hardest thing for an automation framework to fake is a human nervous system. Scripts act with a precision and consistency that people never manage.
- Form fields get filled instantaneously or with perfectly uniform inter-keystroke intervals, where real typing is jittery and bursty. Keystroke dynamics quantify this directly.
- The mouse teleports to element centers or moves in dead-straight lines, instead of the curved, corrective, slightly overshooting paths humans produce. Mouse-movement analysis turns this into a score.
- Page-to-action latency is either superhuman, submitting a multi-field form in under a second, or metronomically regular across many sessions.
- Pointer, touch, and device-orientation events are absent on a client that claims to be a mobile browser.
// Collect timing spread across keystrokes; near-zero variance is a red flag.
let last = 0, gaps = [];
input.addEventListener('keydown', e => {
const now = performance.now();
if (last) gaps.push(now - last);
last = now;
});
// A human produces a wide, irregular distribution of gaps.
// A script that types via dispatchEvent produces gaps clustered
// around a single value or exactly zero.
Behavioral evidence is powerful precisely because patching it requires simulating humanity, not editing a property. It is also why behavioral biometrics tend to survive stealth plugins that defeat the static checks.
Network and server-side signals
Everything above lives in the browser, which means an attacker can see and manipulate it. Server-side signals are harder to reach because the client never observes them.
| Signal | What it reveals |
|---|---|
| TLS fingerprint (JA4) | The real network stack, which often betrays a scripting library despite a spoofed User-Agent |
| HTTP/2 frame ordering | Header and settings-frame patterns unique to non-browser clients |
| IP reputation | Data-center or residential-proxy origin behind the request |
| Request cadence | Fleet-wide rate patterns invisible from any single session |
The most valuable move is cross-layer correlation. A JA4 fingerprint that maps to Go’s HTTP client while the User-Agent claims Chrome 130 is a contradiction the browser layer cannot explain away. Because the client cannot observe its own TLS fingerprint, it cannot easily keep the two stories consistent.
Frequently asked questions
Is navigator.webdriver still a reliable signal?
It reliably catches unpatched automation, but most serious operators disable it, so treat a true value as strong evidence and a false value as no evidence either way. Never rely on it alone.
Can stealth plugins hide all automation signals?
No. Stealth plugins patch the well-known flags, but they cannot fully reconcile timing, environment, and behavioral evidence at once. Layered detection wins because a patch that fixes one leak often opens another.
Should I block on a single automation signal?
Rarely. A single tell produces false positives against legitimate testing tools and accessibility software. Combine multiple weak signals into a score and reserve hard blocks for high-confidence combinations.
The pattern that holds across all of these is layering. Any individual tell can be patched, spoofed, or explained by a legitimate edge case, so a robust detector never bets the verdict on one. It gathers static flags, environment contradictions, behavioral evidence, and server-side network signals, then asks whether the whole picture is internally consistent. Automation that patches the obvious flags still struggles to move a mouse like a person or match its TLS stack to its headers. For a deeper look at the frameworks themselves, see our guide to detecting Selenium, Puppeteer, and Playwright, or test your own traffic in the playground.
Run it yourself
Prynt is open-source, self-hostable device intelligence — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics you own end to end.