Skip to content
Anti-Bot

What Is Headless Browser Detection?

Pim

Pim · Scrappey Research

May 31, 2026 5 min read

What Is Headless Browser Detection? — conceptual illustration
On this page

Headless browser detection is the set of probes anti-bot systems use to distinguish a headless or instrumented Chrome session from a real user's browser. A "headless" browser is a real browser running with no visible window, usually driven by code; "instrumented" means it is being controlled by an automation tool. Plain Puppeteer or Playwright leaks at least a dozen detectable signals out of the box — navigator.webdriver set to true (a flag browsers raise when automation is in control), a missing chrome.runtime object, a predictable plugins list, and blank or always-identical canvas output (the image a page draws to fingerprint your graphics stack). Stealth patches close most of the easy tells; the hard ones survive into 2026.

Quick facts

Easiest tellnavigator.webdriver === true
Common tellsMissing chrome.runtime, plugin array length, languages mismatch
Hard tellsCanvas/WebGL fingerprint, Function.toString() on patched APIs
Stealth toolsplaywright-stealth, puppeteer-extra-plugin-stealth, Camoufox, PatchRight
2026 realityStealth patches that leave toString() trails (Kasada catches these)

The easy tells

These are the giveaways a detector can catch with a single line of JavaScript. By default, Puppeteer and Playwright launch with navigator.webdriver === true (the automation flag is on), window.chrome missing or stripped out, an empty navigator.plugins list, and an HTTP language header that does not match what navigator.languages reports inside the page. Stealth plugins patch all of them — but the patches themselves are detectable (see below).

The complete signal inventory

Twelve signals modern anti-bot scripts inspect for headless detection, grouped by how cheap they are to spoof (fake convincingly):

SignalWhat's checkedSpoof cost
navigator.webdriver=== true in unmodified Playwright/Puppeteer/SeleniumTrivial JS override (but see toString check below)
User-Agent "HeadlessChrome"Default headless Chrome substringTrivial — one line
navigator.pluginsEmpty array in default headlessTrivial JS override
navigator.languagesLength 1 in default headless vs typical 2-3Trivial
WebGL rendererSwiftShader / llvmpipe = no GPUMedium — engine-level patch needed
AudioContext fingerprint~3 known headless-Linux hashesMedium — virtual audio device or engine patch
Canvas fingerprintStable per-machine; headless on Linux produces a small clusterHard — PerfectCanvas replay required
CDP runtime artifactswindow.cdc_* keys, Runtime.evaluate timingHard — undetected-chromedriver patches, breaks on update
Function.toString() inspectionEvery JS-patched method returns its source, not [native code]Very hard — needs engine-level patch
Permissions API quirksNotification.permission === 'default' in headless on a denied-notifications profileMedium
Mouse + scroll absenceZero mouse events before clickMedium — synthesize Bezier-curve movement
requestAnimationFrame cadenceHeadless renders at fixed 60Hz with no vsync jitterHard — engine-level

Here is the key idea. A stealth plugin patches 5–10 of these from inside JavaScript — but the Function.toString() check listed above defeats every JS-layer patch at once, because in JavaScript you can ask any function to print its own source code. A real browser API prints [native code]; a patched one prints the replacement, exposing the patch. Patching below JavaScript, inside the browser's own C++ engine (Camoufox, CloakBrowser, PatchRight) is the only durable answer in 2026.

Toolchain status in 2026

Where each stealth toolchain stands against the 12-signal inventory above:

ToolApproachDefeats
Vanilla Playwright/PuppeteerNoneNothing — block-grade on first request
puppeteer-extra-stealthJS-layer patches (~17)Easy tells; loses to toString inspection
undetected-chromedriverBinary + JS patchesEasy + medium tells; loses to toString and Canvas
SeleniumBase UC modeWraps UC; adds Turnstile auto-clickSame as UC, friendlier API
PatchRightPatches Playwright Python source — patches never exist as JSEasy + medium + toString. Loses to deep Canvas/WebGL only at enterprise tier.
CamoufoxFirefox fork with C++ engine patches + real-machine profile DBAll 12 signals. Hyphenation-dictionary check can still expose it as Firefox.
CloakBrowserChromium fork with 49 C++ patchesAll 12 signals. reCAPTCHA v3 ~0.9 score.

The dividing line is simple: are the patches above or below the JavaScript engine? Above (Playwright, Puppeteer-extra, UC, SeleniumBase) means Function.toString() can read the patch and detect it. Below (PatchRight, Camoufox, CloakBrowser) means the patch is baked into the browser binary and is invisible to JavaScript inspection. Production scraping in 2026 picks tools from the bottom of this list.

The medium tells

These take a little more work to catch than a one-line flag check. Headless Chrome ships with a slightly different default set of fonts than desktop Chrome. The HeadlessChrome string appears in the user agent unless you override it. Asking the graphics API for its hardware name, WebGLRenderingContext.getParameter(UNMASKED_RENDERER), returns "Google SwiftShader" (a software renderer, i.e. no real GPU) on headless instead of a genuine GPU name. The permissions API returns "denied" for notifications without ever prompting the user. Each of these is patched in modern stealth tools.

The hard tells (2026)

The current frontier is meta-detection — catching the act of patching itself rather than any one fingerprint. Anti-bot systems call Function.prototype.toString() on patched native APIs to see whether they return function () { [native code] } (a genuine browser function) or the stealth tool's replacement code (a giveaway). playwright-stealth fails this check; Kasada catalogs the patch signatures and blocks on a match. The 2026 answer is to patch the browser source itself (Camoufox, PatchRight) so there is nothing in the JavaScript runtime for toString() to inspect.

Code example

javascript
// Detect headless from the page side
const tells = {
  webdriver: navigator.webdriver,
  pluginCount: navigator.plugins.length,
  chromeRuntime: typeof window.chrome?.runtime,
  webglVendor: (() => {
    const gl = document.createElement('canvas').getContext('webgl');
    const ext = gl?.getExtension('WEBGL_debug_renderer_info');
    return gl?.getParameter(ext?.UNMASKED_RENDERER_WEBGL);
  })()
};
// Real Chrome: webdriver=false, plugins>0, chrome.runtime='object', GPU string
// Headless: webdriver=true, plugins=0, chrome.runtime='undefined', 'SwiftShader'

Next in How automation gets detected · 3 of 6

The single oldest and simplest flag, and why it is still checked.

What Is navigator.webdriver?

Related terms

Concept map

Concept map

How Headless Browser Detection connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Does playwright-stealth actually work?

For soft targets, yes. Against Cloudflare bot management, Kasada, or DataDome, no — its patch signatures are catalogued and detected through Function.toString() inspection (asking a function to print its source and noticing it is not native browser code).

Is Camoufox detectable?

Less so than stealth-patched Chromium. It is a Firefox fork with anti-fingerprinting built into the source code, so there are no runtime patches for toString() to find. It can still be caught through behavioral signals — like the absence of mouse movement — if you do not emulate them.

Should I just use a real browser session via CDP?

For low volume, yes — attach to a normal Chrome running a real user profile through CDP (Chrome DevTools Protocol, the wire interface tools use to drive Chrome). For high volume, the operational cost of managing real browsers outweighs the detection cost of a hardened headless setup or a managed scraping API.

Is there ever a case where vanilla Playwright is enough?

Yes, on sites with no anti-bot protection — most internal tools, documentation sites, marketing landing pages, and small e-commerce. Past roughly 10% of the public sites you encounter, you hit detection of some kind; even Cloudflare's Bot Fight Mode blocks vanilla Playwright using the datacenter-IP heuristic (flagging traffic from server data centers rather than home connections). For anything advertised as production scraping, start with a patched variant.

Last updated: 2026-05-31