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.

Rule of thumb: if a detection can be defeated by setting a property, it will be. Durable detection targets things the attacker would have to rewrite the browser engine or model human physiology to fake.

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).

What to log per session

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

{
  "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 Sentinel handles this

The automationDetected field in the device-intel response is a composite of the signals above plus ~15 others, computed in a single client-side SDK with no measurable performance cost. The browserTampering float covers the well-stealthed cases where the framework hid its properties but the engine is still patched. Joined with residentialProxy and antidetectBrowser, detection against modern Puppeteer/Playwright stacks runs above 95% with a sub-0.1% false-positive rate on real consumer traffic.

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.