How to detect headless browsers and automation on your signup page
The browser signals that give away Puppeteer, Playwright and Selenium signups, why each one can be faked on its own, and how to combine them without blocking real people.
Why bots use real browsers now
A few years ago most signup bots were plain HTTP scripts posting your form directly. Those are easy to stop: they don't run JavaScript, so anything your page computes in the browser never arrives. Today most serious bots drive a real browser through Puppeteer, Playwright or Selenium, often headless, sometimes through an anti-detect browser that disguises its fingerprint. They run your JavaScript, render your page and fill in your form like a person would.
Detecting them means looking for the places where an automated browser differs from one a human is using. None of the signals below is proof on its own — each can be patched by a determined operator — but they are cheap to collect and they stack.
Signal 1: the WebDriver flag
Browsers under WebDriver-style automation set navigator.webdriver to true. That is a deliberate part of the WebDriver standard, and default setups of Selenium, Puppeteer and Playwright expose it.
const automated = navigator.webdriver === true;
“Stealth” plugins delete or override it, so its absence proves nothing. Its presence, though, is close to conclusive: real users essentially never have it.
Signal 2: the headless user agent
Headless Chrome identifies itself as HeadlessChrome in its user-agent string unless the operator overrides it. Check it on the server too, from the request header, since the agent string the browser reports in JavaScript can differ from what it sends.
Signal 3: a DevTools-protocol client attached
Puppeteer and Playwright control Chrome through the DevTools protocol. When a protocol client listens to console events, logging an object makes the browser serialize it — including reading properties a normal page would never touch. A getter that records whether it was read is a well-known way to notice this:
function devtoolsProtocolAttached() { let read = false; const e = new Error(); Object.defineProperty(e, "stack", { get() { read = true; return ""; } }); console.debug(e); return read; }
It also fires when a developer has DevTools open, so give it moderate weight rather than treating it as a verdict.
Signal 4: software rendering and odd windows
Headless browsers on servers have no GPU, so WebGL falls back to a software renderer such as SwiftShader or llvmpipe. You can read the renderer name through the WEBGL_debug_renderer_info extension where the browser exposes it. Some headless setups also report a window of zero width or height. Virtual machines and remote desktops share these traits, so they are weak signals on their own.
Signal 5: a fingerprint that contradicts itself
Anti-detect browsers exist to look like many different real browsers. They do it by overriding properties, and overrides are rarely perfectly consistent. Things worth comparing:
- The operating system in the user agent versus
navigator.platformand User-Agent Client Hints. - The browser family in the user agent versus features only that engine has.
- The installed fonts versus the fonts that operating system ships with.
- A canvas drawn twice in a row producing two different images — a sign of injected noise.
Notification.permissionsaying “denied” while the Permissions API says “prompt”.
Each contradiction is weak alone, because privacy extensions and unusual setups produce some of them too. Two or three together is a strong pattern.
Signal 6: the token that never came back
A simple and robust technique: have the signup page's script request a short-lived, single-use token and put it in a hidden form field, then verify it on the server. A bot that skips your JavaScript arrives without a valid token; one that replays an old token arrives with a used one. You don't need to know how the bot works to notice it never ran your page properly. Keep the token single-use, bind it to your site, and let it expire in minutes.
Signal 7: behaviour, not fingerprints
Fingerprints can be faked; behaviour is harder. A person reads the form, moves the pointer and types. A script fills the fields and submits within a few hundred milliseconds of the page loading, with no trusted input events at all.
The reliable measurement needs no extra software: your agent issues the visitor token when the page loads, and your server compares that timestamp with the moment of the check. Under about a second from load to signup is a script. When the agent can also report trusted input events, an instant submit with zero pointer, key or touch events is a second, independent confirmation.
A caveat on signal 3: in current Chrome the DevTools trick only fires when the automation client subscribes to console events, so treat it as a bonus rather than the main detector.
Combine, don't block on any single signal
Treat each signal as points in a score and decide on the total. A reasonable starting weighting, from strongest to weakest:
| Signal | Weight | Why |
|---|---|---|
navigator.webdriver true | high | Almost never true for real users |
| HeadlessChrome user agent | high | Only headless builds send it |
| Token missing or replayed | high | The page's script never ran, or ran once for many signups |
| DevTools protocol attached | medium | Developers with DevTools open trigger it too |
| OS or browser contradictions | medium | Anti-detect overrides, but also some extensions |
| Software renderer, zero window | low | VMs and remote desktops share them |
Pair the browser signals with server-side ones — signups from cloud IP ranges, bursts from one subnet, accounts linked to the same device — and very few bots will look clean on all of them at once.
Things not to do
- Don't block on the absence of plugins or languages alone. Privacy-hardened browsers strip them.
- Don't ship your detection logic as obvious named checks that an operator can grep for and patch in one go; collect raw facts in the browser and decide on the server.
- Don't trust anything the browser reports blindly. The client is under the bot's control. Verification happens on your server, with your secret.
How Attrelix does it
The Attrelix agent collects these browser facts and a single-use visitor token on your signup page; the server combines them with network, inbox and linked-account signals into one score. All the weights are public in the docs, and no single soft signal can push a signup to review by itself.
Summary
- Modern bots run real browsers; look for where automation differs from a human.
- WebDriver, headless UA and missing tokens are strong; the rest are weak but stack.
- Decide on the server, from a score, never from one browser-side flag.