All articles Bot detection

Detecting Selenium, Puppeteer and Playwright

Selenium, Puppeteer and Playwright drive most of the automation that hits login forms, checkout flows and scraping targets. They are legitimate testing tools, which is exactly why they are abused: they are free, well documented, and can drive a real browser engine that renders like a human’s. The detection problem is not “is this a browser” but “is a human sitting behind this browser, or a script”.

Naive detection looks for a single tell and loses the moment someone installs a stealth plugin. Durable detection combines client-side artifacts, input behavior, and server-side transport signals so that patching any one layer still leaves the others intact. This article walks through what each framework leaks, what survives evasion, and how to weight the evidence.

What each framework actually leaks

The three tools reach the browser differently, and that changes what you can observe.

  • Selenium drives browsers through the W3C WebDriver protocol, usually via chromedriver or geckodriver. Classic Selenium sets navigator.webdriver to true and, in older chromedriver builds, injected variables such as cdc_ properties onto the document object.
  • Puppeteer speaks the Chrome DevTools Protocol (CDP) directly. There is no separate driver binary; the automation attaches to the browser’s debugging endpoint. Default launches run headless and expose a HeadlessChrome token in the user agent unless overridden.
  • Playwright also uses CDP for Chromium and custom protocols for Firefox and WebKit. It patches many obvious leaks itself, uses persistent contexts, and can run headed, which removes the easiest headless tells.

Across all three, the shared weak point is CDP. When a browser has an active DevTools client attached, certain behaviors change subtly: Runtime.enable alters how exceptions serialize, and the presence of a debugger affects console and stack-trace formatting. Detecting an attached CDP session is more robust than checking a single boolean because stealth plugins that null out navigator.webdriver do not necessarily hide the protocol they depend on.

Client-side signals worth collecting

Client-side checks are the richest source, provided you assume the environment is hostile and never trust a value you cannot corroborate.

  • navigator.webdriver — cheap, catches unsophisticated scripts, defeated instantly. Keep it, weight it low.
  • Headless rendering artifacts — missing chrome runtime object, empty navigator.plugins, unusual navigator.languages, or a permissions API that returns inconsistent states (for example Notification.permission of denied while the query API reports prompt).
  • CDP-attached behavior — error serialization timing, stack traces that appear when Runtime.enable is active, and property access patterns that only occur under instrumentation.
  • Canvas, WebGL and audio consistency — automation on servers often exposes software renderers like SwiftShader or llvmpipe in the WebGL UNMASKED_RENDERER string. Cross-check these against the claimed user agent. See our WebGL fingerprinting guide and how canvas fingerprinting works for the collection details.
  • Function-source tampering — stealth frameworks override native functions such as Function.prototype.toString to hide their patches. A function that claims to be native but has an unexpected source length or throws under proxy inspection is a strong signal.

A small consistency check illustrates the idea better than prose:

function rendererLooksAutomated() {
  const gl = document.createElement('canvas').getContext('webgl');
  const ext = gl && gl.getExtension('WEBGL_debug_renderer_info');
  const renderer = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : '';
  const ua = navigator.userAgent;
  const claimsDesktopGpu = /Windows|Mac OS X/.test(ua);
  const softwareRender = /SwiftShader|llvmpipe|Mesa OffScreen/i.test(renderer);
  return claimsDesktopGpu && softwareRender;
}

The value here is not the boolean but the contradiction: a machine claiming to be consumer Windows hardware while rendering through a headless software rasterizer.

Behavioral and timing signals

The hardest thing for an automation script to fake is human motor behavior. Scripts issue synthetic events with mechanical timing, and this is where behavioral biometrics and mouse movement bot detection earn their place.

  • Input dynamics — real typing has variable inter-key intervals and occasional corrections. dispatchEvent-driven input arrives with near-constant spacing or all at once via value setters that skip keystroke events entirely.
  • Pointer trajectories — human cursor paths curve and overshoot. Puppeteer’s page.click teleports to coordinates with no intervening mousemove.
  • Execution timing — a page loaded, filled and submitted in under a second with no idle time between actions is a machine cadence, not a human one.
  • Event trust — the isTrusted property on events is false for anything a script dispatches. It can be spoofed only by injecting through CDP, which is itself a signal.

None of these is decisive alone. A patient script can add jitter. But adding realistic behavior across every field, on every session, at scale, raises the attacker’s cost, which is the actual goal of detection.

Server-side signals that survive stealth

Client-side code runs in the attacker’s environment and can be modified. Server-side signals are collected on infrastructure you control, so they resist patching. Combine them with the client evidence for server-side and client-side bot detection that degrades gracefully.

SignalWhat automation tends to show
TLS fingerprint (JA4)Node-based tools present TLS stacks that do not match the claimed browser
HTTP header orderWebDriver and HTTP client libraries order headers differently from real Chrome
HTTP/2 fingerprintFrame settings and pseudo-header order diverge from genuine browsers
IP reputationDatacenter ASNs, proxy pools, and residential-proxy exits cluster around automation
Request cadenceFixed intervals, no think time, perfect retry backoff

TLS is especially useful because Puppeteer and Playwright over CDP still drive Chromium’s real TLS stack, but many scraping stacks route through Node’s HTTP layer or intercepting proxies that break the match. Our TLS fingerprinting with JA4 explainer covers how the hash is built and why it is hard to forge without reimplementing the browser’s network stack.

Scoring instead of blocking on one flag

Any single check produces false positives. Privacy browsers strip plugins; corporate proxies rewrite headers; accessibility tools dispatch synthetic events. The right model is a weighted suspect score with reason codes, not a hard gate on navigator.webdriver.

  • Assign each signal a weight based on how hard it is to fake and how often it fires on legitimate traffic.
  • Require corroboration: a headless renderer plus a datacenter IP plus zero input dynamics is a confident bot; any one alone is a maybe.
  • Return reason codes so your fraud team can see why a session scored high and tune thresholds without redeploying.
  • Escalate borderline sessions to a proof-of-work challenge rather than blocking, so a misjudged human can still pass.

This approach ties into stable identity too. A confidence score on the visitor ID tells you whether you have seen this device before behaving like a human, which is often more informative than any single automation tell.

Frequently asked questions

Can you detect Selenium without JavaScript?

Partly. Server-side you can flag WebDriver-style header ordering, TLS fingerprints and request cadence, but the strongest driver-level signals like navigator.webdriver and CDP artifacts require client-side collection.

Does the navigator.webdriver flag still work in 2026?

It catches naive scripts but is trivially patched by stealth plugins, so treat it as one weak signal among many rather than a decision on its own.

How do stealth plugins change detection strategy?

They erase obvious flags, so you shift weight toward things they cannot easily fake: execution timing, input dynamics, TLS and header order, and cross-session device consistency.

The frameworks evolve, and so do the stealth plugins built to defeat detection. That is why no single check holds for long. A layered model that combines client artifacts, human behavior and server-side transport signals raises the cost of evasion at every layer, and a scored, explainable output lets you act on that evidence without punishing the legitimate users who happen to look unusual. Explore the bot detection pillar and the playground to see these signals on live traffic.

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.

Keep reading