Resources Docs Free Blog Contact
Log in Get started
SIG-853 · MASKBREAK RESEARCH
Detection research

How to Detect Puppeteer & Playwright in 2026

Puppeteer and Playwright ship with stealth plugins that defeat every classic check. The signals that still detect CDP-driven automation in 2026, with code.

On this page
  1. Why the classic checks are dead
  2. The five signals that still work
  3. What to log per session
  4. The cases that need extra care
  5. How Maskbreak handles this

If your Puppeteer detection reads navigator.webdriver, you are only catching the bots that didn't install a stealth plugin. Puppeteer and Playwright both drive Chromium through the DevTools Protocol, and the entire public detection surface has a documented counter. The signals that still work in 2026 live below the properties an attacker can flip — in input physics, GPU rendering, and behavior. Here's exactly what to instrument, with the client telemetry to collect.

Puppeteer and Playwright are the two dominant browser-automation frameworks, and between them they power most of the automated abuse hitting web applications today: credential stuffing, scalping, scraping, fake-signup rings, and checkout bots. Both control a real Chromium build over the Chrome DevTools Protocol (CDP); Playwright additionally drives Firefox and WebKit. The detection problem is the same for all of them: the framework is a normal browser engine being puppeteered from outside, so the question isn't "is this Chrome?" — it obviously is — but "is a human or a script sitting at the controls?"

Why the classic checks are dead

Nearly every tutorial on detecting Puppeteer still leads with property probes. Every single one has a counter that ships by default in the tooling attackers actually use (puppeteer-extra-plugin-stealth, playwright-stealth, undetected-chromedriver, rebrowser patches):

  • navigator.webdriver === true — forced to false at startup since 2018.
  • window.chrome.runtime missing — injected by stealth plugins since 2019.
  • Empty navigator.plugins / mimeTypes — spoofed with realistic values.
  • Notification.permission vs permissions-API mismatch — reconciled since 2020.
  • WebGL VENDOR/RENDERER "SwiftShader" / "Google Inc." — overridden via getParameter hooks.
  • CDP-exposed User-Agent, Headless in UA string — patched at launch.
  • navigator.languages empty, mismatched Accept-Language — normalized.

These are not obscure bypasses. They are the out-of-the-box defaults of the most popular stealth packages, which have tens of thousands of GitHub stars between them. If a detection technique is a readable property on navigator, window, or screen, assume it is already spoofed. The detection axis has to leave static properties entirely.

The five signals that still work

1. Input-event entropy (the strongest single signal)

Human input, even fast human input, generates continuous event streams with physical properties that CDP-injected input does not reproduce:

  • Mouse: real mousemove streams have sub-pixel coordinates, acceleration/deceleration curves toward targets, and micro-jitter. page.mouse.move() — even with stealth — produces integer-only coordinates, geometrically perfect or perfectly-eased paths, and identical inter-event timing regardless of distance.
  • Keyboard: per-key dwell time (keydown→keyup) varies widely across keys for humans and clusters tightly for scripts, even when the script randomizes delays.
  • Scroll: real wheel/trackpad scrolls arrive in bursty, variable-velocity clusters; programmatic scroll is absent or perfectly uniform.

The highest-value rule is almost embarrassingly simple: a session that completes a sensitive action (login, signup, checkout) with zero preceding mousemove or touchmove events is automation by default. Layer a Shannon-entropy check on the movement stream on top of that, and CDP-driven sessions fail it near-universally.

2. CDP runtime signature

The DevTools Protocol leaves a small, persistent timing signature regardless of stealth patching — the classic proof-of-concept accesses a getter on an object passed to console.debug, which the protocol serializes only when a debugger is attached, producing a measurable delta. The specific technique needs periodic refreshing (Chromium patches the most public ones), but the underlying gap is architectural. Use it as a high-confidence supporting signal joined with input entropy, never alone.

3. GPU / canvas rendering deltas

CDP-driven Chromium renders measurably differently from a real user's browser, and stealth plugins don't touch the GPU pipeline because faking a renderer creates worse inconsistencies than it hides:

  • Subpixel text positioning in canvas deviates by stable, machine-predictable amounts.
  • WebGL readPixels() on a fixed calibration scene differs by stable byte-level deltas between headless/CDP and genuine headful sessions.
  • Bezier-curve anti-aliasing in 2D canvas paths shows characteristic patterns.

A hash table mapping "rendering of a known scene → automation signature" is one of the cleanest surfaces available, and it holds up against fingerprint-normalized commercial automation.

4. High-precision timing

requestAnimationFrame cadence and performance.now() resolution behave differently under CDP control, especially without a real compositor. rAF fires on a software-generated cadence with a distinct jitter profile versus vsync-aligned headful. Modern stealth tooling tries to spoof this with mixed success — treat it as a tie-breaker, not a primary.

5. Behavioral baselines vs the population

Signals real users vary on and scripts don't: keystroke-dwell variance, form-fill order entropy (humans tab and edit out of order; bots fill in source order), and page-dwell distribution (bots are bimodal — instant or exactly the configured wait; humans are continuous).

Try it

Put your own browser on the bench: the free scanner on the homepage returns the verdict the API returns, with every reason listed.

Open the scanner

What to log per session

The minimum useful client-side telemetry, captured before the action you care about:

json
{
  "input": {
    "mouseMoveCount": 42,
    "mouseMoveEntropy": 4.13,
    "trajectorySmoothness": 0.78,
    "integerOnlyCoords": false,
    "touchEventCount": 0,
    "keyDwellMeanMs": 84,
    "keyDwellStdDev": 33,
    "scrollEventCount": 7,
    "scrollEntropy": 2.6
  },
  "render": {
    "canvasFingerprint": "f3a9...",
    "webglReadPixelsHash": "b21c...",
    "rafJitterMs": 0.42
  },
  "timing": {
    "performanceNowResolutionMs": 0.005,
    "actionDelayMs": 1842
  }
}

The input block alone powers the detection in most cases. render is the high-confidence corroborator. timing breaks ties. Score the vector; never gate on a single field.

Score, then route

Return a 0–1 automation confidence and act on bands rather than a hard boolean:

  • 0.0–0.3: allow.
  • 0.3–0.6: step up — email confirm, 2FA, a challenge.
  • 0.6–0.9: hard challenge or honeypot route.
  • 0.9–1.0: block, log, and blacklist the device.

The cases that need extra care

Headless-as-a-service

Browserless, BrowserBase, and ScrapingBee run hardened, fingerprint-normalized Chromium behind rotating residential proxies. Static-property and IP checks fail against them; behavioral signals do not, because no amount of fingerprint work gives a script human input entropy. Pair automation detection with residential-proxy detection to catch the whole stack.

Don't confuse this with antidetect browsers

Puppeteer/Playwright are automation frameworks; antidetect browsers like Kameleo and GoLogin are human-operated tools for running many identities. They share some spoofing but the detection emphasis differs — automation is a behavioral problem, antidetect is a fingerprint-consistency problem. Many real operations combine both.

How Maskbreak handles this

The botDetected boolean in the deviceIntel response is a composite of the signals above plus ~15 others, computed in a single client-side SDK with no measurable performance cost; it surfaces as device.automation with the automation_detected reason code. The tamperingScore float (and its browserTampering boolean) covers the well-stealthed cases where the framework hid its properties but the engine is still patched. Joined with proxied and device.antidetect, detection holds up against modern Puppeteer/Playwright stacks even with stealth plugins, because the signals above are read beneath the patched JS surface rather than from the properties stealth plugins rewrite.

The free tier (1,000 requests/hour, no card) covers the highest-value paths — login, signup, checkout, password reset. Most teams discover their CAPTCHA-only defense was missing 30–60% of real automation. You can watch the signal live on your own connection with the scanner on the homepage.

Detecting Puppeteer and Playwright in 2026 is not a property check — it is a behavioral and rendering-level analysis that assumes the attacker patched everything cosmetic. The signals that survive are the ones you can't flip with a config option, and that's exactly why they keep working.


FAQ

Questions people ask

How do I detect Puppeteer if it uses the stealth plugin?
The stealth plugin patches static properties (navigator.webdriver, window.chrome.runtime, plugin arrays, WebGL vendor strings) but not how CDP drives the engine. Detection relies on signals stealth doesn't reach: input-event physics, the GPU rendering signature of CDP-controlled Chromium, and behavioral baselines. A form submitted with zero preceding mousemove events and integer-only pointer coordinates is automation regardless of what navigator.webdriver says.
Is Playwright harder to detect than Puppeteer?
Not meaningfully. Both drive Chromium (Playwright also drives Firefox and WebKit) through a DevTools-style protocol and leak the same behavioral and rendering signals. Detection that keys on behavior rather than the framework catches both at the same rate.
Can server-side detection catch Puppeteer, or do I need client-side JS?
Server-side signals (TLS fingerprint, HTTP/2 frame ordering, header order, IP reputation) catch naive automation but sophisticated operators normalize all of them and route through residential proxies. The signals that survive — input entropy, GPU rendering deltas, high-precision timing — are only observable client-side. The strongest setups collect a small client-side payload and evaluate it server-side.
Will detecting automation block real users or accessibility tools?
Not if you score on a vector instead of a single signal. Screen readers run in a real headful browser and produce normal input streams, so their signature is distinct from CDP-driven sessions. Touch-only users legitimately have zero mousemove events, which is why detection combines input entropy with rendering and timing signals. Well-tuned detection runs below a 0.1% false-positive rate.
How do I detect headless-browser-as-a-service like Browserless or BrowserBase?
Managed providers run hardened, fingerprint-normalized instances behind rotating residential proxies, so static-property and IP checks fail. The behavioral signals still work: no fingerprint normalization gives a CDP session human mouse-trajectory entropy or keystroke-dwell variance. Combine automation detection with residential-proxy detection to catch the full stack.
How often do Puppeteer detection techniques need updating?
Static-property detections break constantly — assume any check that reads a navigator or window property has a stealth counter within months. Behavioral and rendering detections are far more durable because defeating them means modeling human input physics or swapping the GPU pipeline. Maskbreak maintains the fragile CDP-runtime checks so integrators don't have to, and leans on the durable behavioral signals as primaries.
Get started

Run the same check on your traffic

The free scanner on the homepage returns the verdict the API returns: network and device signals in one call, with every reason listed. What each signal means: <a href="/bot-detection">bot detection</a> and <a href="/device-fingerprinting">device fingerprinting</a>.

Get started freeRead the API docs