A headless browser is a real browser running without a visible window. It executes JavaScript, renders pages, manages cookies, and passes almost every "is this a browser?" test, because it is one. That is what makes it the standard vehicle for serious automation in 2026: scrapers, credential stuffers, signup farms, and the cloud browser fleets behind AI agents all run headless. Detecting them used to mean checking a short list of well-known tells. Most of that list stopped working when Chrome shipped its new headless mode, so the useful question now is which signals survived.
The framework-specific companions go deeper on the tooling: Selenium, Puppeteer, and Playwright each leak in their own ways, and AI agent detection covers the agentic case. This article covers what they share.
What a headless browser is
Every major engine ships a headless mode. Chromium has had one since Chrome 59 in 2017; Firefox and WebKit followed. Headless mode exists for legitimate reasons that have nothing to do with abuse:
- Automated testing. CI pipelines run browser test suites headless because build machines have no display.
- Rendering services. PDF generation, screenshot APIs, link unfurlers, and search engines rendering JavaScript-heavy pages.
- Synthetic monitoring. Uptime and performance checks that load real pages on a schedule.
The same properties that make headless browsers useful for testing (scriptable, parallelizable, no display required) make them the default vehicle for abuse. A scraper that needs JavaScript rendering, a stuffing operation that needs to execute your login page’s client code, or a signup farm that needs to look like Chrome all reach for the same stack: a headless Chromium driven by Puppeteer or Playwright, usually with a stealth layer on top.
So the detection question is not really whether the client is a browser. It is whether there is a person behind it.
The 2026 headless landscape
Three shifts define the current landscape.
New headless mode closed the easy gap. Until 2023, headless Chrome was a separate implementation that genuinely differed from the desktop browser: no window.chrome object, no plugins, different font rendering, missing media codecs. In Google’s own words it was a second browser shipped inside the Chrome binary, a lightweight wrapper around Chromium’s //content module rather than Chrome itself. Chrome 112 made the new headless mode (the real Chrome binary with the window suppressed) generally available in early 2023, Chrome 120 split the old implementation out as a separate chrome-headless-shell download, and Chrome 132 removed it from the Chrome binary altogether in January 2025 (Chrome for Developers). A new-headless Chrome is, at the feature level, the same browser your users run, so the feature-presence checks that worked for a decade stopped working on their own.
The browsers moved to the cloud. Operators increasingly rent headless capacity instead of running it. Hosted browser farms like Browserbase and Browserless sell Chrome-by-the-hour with residential egress and stealth presets, and AI agent products run their sessions on the same infrastructure. The browser is real, patched, and professionally maintained, which pushes the durable evidence toward the IP layer and session behavior.
Stealth became table stakes. Off-the-shelf stealth patches (puppeteer-extra-plugin-stealth historically, rebrowser-style patches and CDP-minimal drivers more recently) fix the famous JavaScript tells before the first page loads. Any detector that relies on a single well-known artifact is testing whether the operator skipped a README step.
The classic tells, and why most are gone
These are the checks every blog post lists, in roughly the order they stopped being sufficient:
- The User-Agent token. Default headless Chrome announces itself with
HeadlessChrome/in the UA string. Every framework lets you override it in one line. Still worth checking because it catches the laziest tier, but absence proves nothing. navigator.webdriver. The spec-mandated flag istrueunder automation. Selenium, Puppeteer, and Playwright all expose switches or patches that unset it. See the Selenium article for the related$cdc_ChromeDriver artifact.- Missing plugins, languages, and
window.chrome. Old-headless tells. New headless mode reports plausible values for all three. - Permission inconsistencies. The classic
Notification.permission === 'denied'whilepermissions.queryreportspromptcontradiction has been patched both by Chrome and by every stealth layer. - Default viewport. Out of the box, headless sessions render at fixed sizes like 800×600 or 1280×720 with
devicePixelRatioof 1 and zero screen-position offsets. Trivial to randomize, and stealth presets do, but real-device viewport distributions are hard to imitate jointly with the rest of the device claim.
All five share the same weakness: each is cheap to check and cheap to spoof. They still earn their place at the top of a detection funnel, since a meaningful fraction of automation never configures them, but none of them justifies a verdict on its own anymore.
What still works
The signals that hold up in 2026 share one property: they are expensive for the operator to fake consistently, because they come from layers the automation stack does not fully control.
1. Rendering and hardware ground truth
A headless browser on a server has no GPU, or a virtualized one. Chrome falls back to SwiftShader (or llvmpipe on Linux) for WebGL, and that renderer string does not match the consumer hardware the session claims to be. Neither does the rendering timing of a deliberately expensive canvas or WebGL workload. Spoofing the renderer string is easy; making a software rasterizer hit GPU-class frame times is not. Canvas fingerprinting and browser fingerprinting techniques cover the underlying probes.
Server environments also leak through the long tail of the platform: minimal Linux images with a handful of fonts while claiming Windows, zero speechSynthesis voices, no media input devices, battery API absent on a claimed laptop. Any single gap is deniable. A dozen of them, jointly, are not a consumer device.
2. Automation-protocol artifacts
The frameworks drive the browser over an automation protocol, usually the Chrome DevTools Protocol, and the protocol leaves marks: serialization side effects observable from page JavaScript, injected bindings like Playwright’s __playwright__binding__, and exposed-function residue. Chrome patched the best-known Runtime.enable side effect in 2025 and CDP-minimal drivers emerged in response, so this is an arms race measured in months. It remains one of the highest-precision signals when it fires. CDP detection covers the arms race in depth, and the Puppeteer and Playwright articles cover the current state per framework.
3. Behavioral absence
Headless sessions do things no human session does: navigate with zero pointer movement, fill forms with perfectly uniform inter-key timing or a single programmatic value assignment, scroll in exact pixel increments, and fire click events with no preceding mousemove trail. Scripted “human-like” jitter exists, but generated motion has its own statistical signature, and real pointer traces turn out to be surprisingly hard to synthesize at scale. Behavioral scoring is also the layer that keeps working when the environment is perfectly spoofed, because the operator has to fake it per session, per page, indefinitely.
The newest research points the same way. A 2026 UC Davis measurement study of seven AI browsing agents found that browser fingerprints alone barely separated one agent from another, because many run the same underlying Chromium build, while their typing, scrolling, and mouse dynamics were distinctive enough to tell the agents apart from humans and from each other (Wang, Shafiq, and Vekaria, FP-Agent, 2026). As automation converges on real browsers, the behavioral layer is the one gaining discriminative power rather than losing it.
4. Network-layer contradictions
The TLS fingerprint of the actual client, the ASN of the source IP, and the claimed browser must agree. A session claiming consumer Chrome on Windows, arriving from a Hetzner ASN with a Linux TCP fingerprint, has already answered the question regardless of how clean its JavaScript environment looks. Residential proxies blunt the IP check; residential proxy detection covers what survives.
5. Cluster behavior
One operator running five hundred headless sessions cannot make them independent. They share workload shapes, timing cadence, configuration residue, and target endpoints, even when each session’s fingerprint is randomized. Randomization itself becomes the tell: five hundred sessions whose fingerprints are too evenly distributed look nothing like five hundred real users, whose devices cluster heavily around popular hardware. Per-session detection misses this; cross-session clustering is where sophisticated headless fleets actually get caught.
A practical detection checklist
Ordered by cost, the way a production pipeline should run:
- Free, server-side. UA string sanity,
Sec-Fetch-*header coherence, TLS fingerprint vs claimed browser, IP ASN category. Catches unmodified scripts and lazy configurations. - Cheap JS probes.
navigator.webdriver, known framework globals and bindings, permission and plugin coherence. Catches default-configuration headless. - Environment ground truth. WebGL renderer and render timing, font and voice census, screen geometry plausibility, joint OS/hardware consistency. Catches stealth-patched headless on server hardware.
- Behavioral scoring. Pointer presence and dynamics, typing cadence, scroll texture, interaction-before-action ordering. Catches clean environments with no human in them.
- Cluster analysis. Cross-session signatures, fingerprint-distribution anomalies, shared operational cadence. Catches the fleet even when individual sessions pass.
Each tier should feed a score, not a verdict. A session that fails tier 1 never needs tier 4; a session that passes tiers 1–3 with an empty behavioral channel is exactly the case tiers 4 and 5 exist for.
Legitimate headless traffic
Some headless traffic you want. Search engines render JavaScript headlessly; your own synthetic monitors do too; partners may run authorized automation against your APIs. The right structure is the one described in bot management: verify the traffic that identifies itself (reverse DNS for crawlers, Web Bot Auth signatures where supported, allow-listed keys for partners), give it explicit policy, and reserve detection-based enforcement for traffic that claims to be human and is not. A blanket “block anything headless” rule tends to break your own monitoring before it inconveniences an attacker.
How Foil detects headless browsers
Foil’s SDK collects across all five tiers during normal page interaction and returns a sealed server-side decision. Headless sessions surface with a bot verdict and, when the driver is identifiable, attribution labels naming the tool (puppeteer, playwright, selenium); when the driver is unclear the verdict still lands on the environment and behavioral evidence, attributed to the actor automation. The application can therefore apply per-route policy rather than a blanket block:
import { Foil, safeVerifyFoilToken } from '@abxy/foil-server';
const client = new Foil({ secretKey: process.env.FOIL_SECRET_KEY });
const result = safeVerifyFoilToken(sealedToken, process.env.FOIL_SECRET_KEY);
if (!result.ok) return next(); // no or invalid token: apply your default policy
const { decision, session_id } = result.data;
if (decision.verdict === 'bot') {
// which framework? read the attribution labels server-side
const session = await client.sessions.get(session_id);
const tool = session.attribution.labels.find((l) => l.kind === 'tool');
log('automation detected', tool?.value); // 'puppeteer', 'playwright', 'selenium', ...
// automation claiming to be a person
return res.status(403).json({ error: 'automation_not_permitted' });
}
if (decision.verdict === 'inconclusive') {
return requireProofOfWork(req, res, next);
}
For the policy layer that decision feeds, see bot management and bot mitigation. For what each framework leaks individually, the Selenium, Puppeteer, and Playwright articles pick up from here.
Further reading
- Chrome for Developers, Removing the old Headless from Chrome (the 112/120/132 milestones): developer.chrome.com/blog/removing-headless-old-from-chrome
- Chrome for Developers, Chrome’s Headless mode: developer.chrome.com/docs/chromium/new-headless
- Wang, Shafiq, Vekaria, FP-Agent: Fingerprinting AI Browsing Agents (UC Davis, 2026): arxiv.org/abs/2605.01247
- Chromium, headless/README.md (the //content architecture): chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
- Antoine Vastel, Detecting headless Chrome (the original tells, for history): antoinevastel.com/bot%20detection/2018/01/17/detect-chrome-headless-v2.html