The navigator.webdriver trick stopped working five years ago. Modern Puppeteer and Playwright ship with stealth plugins out of the box. Here's what actually catches them in 2026 — and why most homemade detection code is a waste of time.

If you maintain a public-facing site, you have a Puppeteer problem. It just may not be on your radar yet. The same library used for legitimate headless testing powers most of the scraping, credential stuffing, scalping, and click-fraud traffic on the internet today. Playwright is now a close second — Microsoft's investment plus first-class TypeScript support pulled half the bot industry across.

The bad news: detecting them used to be easy and isn't anymore. The good news: it's still entirely possible if you know what to fingerprint.

Why old detection code fails

For years the canonical Puppeteer detection was a one-liner:

if (navigator.webdriver) blockUser();

That stopped working around 2021. The puppeteer-extra-plugin-stealth package patches navigator.webdriver back to undefined, and a hundred other tells, in two lines:

const puppeteer = require('puppeteer-extra');
puppeteer.use(require('puppeteer-extra-plugin-stealth')());

Playwright has equivalent libraries (playwright-extra, playwright-stealth). The result: every Stack Overflow detection snippet from before 2023 returns clean for any halfway-competent attacker.

What's left to detect. Stealth plugins patch the obvious tells. They cannot patch every browser surface, every timing signal, and every contradiction between claimed identity and actual behavior. Modern detection is the sum of those gaps — a probability score, not a single boolean.

The signals that still work

1. CDP / Devtools Protocol artifacts

Puppeteer and Playwright both attach via the Chrome DevTools Protocol. CDP injects a small number of side effects that are awkward to scrub — accessing window.chrome.runtime.connect behaves differently under CDP than under a real user-launched Chrome. Querying for Runtime.enable handlers, or measuring Error.stack overhead during a thrown error, can reveal an attached debugger with high confidence.

2. Headless rendering quirks

Even --headless=new (the post-2023 modern headless mode) has fingerprintable rendering differences from headed Chrome. Subtle ones: font fallback when a system-installed font is missing, hairline-width antialiasing on diagonal lines, the way requestAnimationFrame ticks when the page is "visible" but no display is attached. None of these are individually conclusive. Together they form a high-confidence headless signal.

3. Behavioral inconsistencies

The cheapest and most resistant signal is human behavior. Real users move the mouse before clicking. They don't fill three form fields in 11 milliseconds. They don't have a perfectly straight scroll velocity curve. They don't trigger focus on the password field at the same exact x/y offset on every page load. Puppeteer and Playwright scripts overwhelmingly do.

4. Network-stack tells

Headless browsers driven by automation rarely come from residential IPs. The vast majority of Puppeteer traffic at scale runs from datacenter ASNs (Hetzner, OVH, AWS, GCP, DigitalOcean) or, when sophisticated, residential proxy networks (BrightData, Smartproxy, NetNut, IPRoyal, ShadowNode). Either is a strong negative signal when combined with the rest.

5. TLS / JA3 fingerprint mismatch

Node's TLS stack is not Chrome's TLS stack. Even when the User-Agent says Chrome/124, Puppeteer's outbound JA3 fingerprint matches the underlying Node.js OpenSSL build, not Chrome's BoringSSL. A discrepancy between the claimed UA and the observed JA3 is one of the most reliable single signals available — and one of the hardest to spoof without rewriting Node's TLS layer.

What about CAPTCHAs?

CAPTCHAs are a tax on real users that modern bots route around. 2Captcha, Anti-Captcha, and CapSolver resolve hCaptcha, reCAPTCHA v2, reCAPTCHA v3, and Turnstile in 8–25 seconds for fractions of a cent. If a Puppeteer script can earn a dollar from your site, the captcha-solving cost is rounding noise.

The point of CAPTCHA was friction. Now it's friction only for legitimate users. We have a whole post on bot detection without CAPTCHA if you want the full argument.

How Sentinel does it

Sentinel runs all five signal categories above on every visit, transparently, and returns a verdict in under 40ms:

  • Bot / automation flag — Puppeteer, Playwright, Selenium, Cypress, headless Chrome, headless Firefox, PhantomJS (still seen).
  • Browser tampering score (0–1) — degree to which the browser surface is internally inconsistent (UA says X, JA3 says Y, plugin list says Z).
  • Antidetect-browser flag — Kameleo, GoLogin, AdsPower, Multilogin, Dolphin{anty}, Octo Browser. These are the "real browser, fake fingerprint" tier above plain Puppeteer.
  • VPN / proxy / residential proxy flag — including the residential pools that scrape & scalp operators rent.
  • Persistent visitor ID — survives incognito, IP rotation, and stealth-plugin patches. Lets you count sessions, not requests.

A minimal integration

Add the SDK script to your page:

<script async src="https://fp.sntlhq.com/agent"></script>

On your server, before any sensitive action — login, signup, search-by-keyword API, add-to-cart — verify the token:

const verdict = await fetch('https://sntlhq.com/v1/evaluate', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_YOUR_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ token: req.body.sentinelToken })
}).then(r => r.json());

if (verdict.details.isBot || verdict.details.tamperingScore > 0.7) {
  return res.status(403).json({ error: 'forbidden' });
}

That's the whole integration. The SDK fires asynchronously on page load, so the token is ready by the time the user clicks anything.

What about your existing rules?

Keep them. Sentinel slots in front of whatever you already have:

  1. Cloudflare / WAF — drops drive-by junk traffic.
  2. Sentinel — drops sophisticated automation, antidetect browsers, and residential-proxy sessions.
  3. Your business logic — rate limits, account-age checks, payment-pattern rules.

Each layer does what it's best at. Removing the bots upstream lets your downstream rules breathe.

Try it

Free API key at sntlhq.com/signup — 1,000 requests/hour, no card. Install the npm SDK, drop in the script, and your Puppeteer/Playwright traffic will start failing within the hour. If you're integrating with Node, the package is @sentinelsup/sdk.