How to Detect Bots with Browser Fingerprinting (2026)
Headless browsers, automation frameworks, and bots leak dozens of fingerprinting signals that
real browsers don't. navigator.webdriver = true is just the start โ SwiftShader GPU,
missing audio hardware, broken plugin arrays, and inconsistent Chrome runtime objects all reveal
automation. Here's the complete technical guide to bot detection via browser fingerprinting.
Why Browser Fingerprinting Is Effective for Bot Detection
Bots โ scrapers, credential stuffers, ad fraud bots, account takeover tools โ typically run in headless browser environments. These environments differ from real user browsers in subtle but detectable ways. Fingerprinting reads dozens of JavaScript API values simultaneously, making it difficult for bots to spoof everything correctly at once.
Even if a bot patches the most obvious signals (like navigator.webdriver),
there are always secondary signals that give it away โ inconsistencies between the claimed
User Agent, the GPU renderer, the audio API state, and the plugin list.
Primary Bot Detection Signals
true by Chrome/Firefox when controlled via WebDriver (Selenium, Puppeteer, Playwright). The single most reliable signal. Many bots patch this, but patching introduces other inconsistencies.window.chrome object with runtime, loadTimes, and other sub-objects. In headless mode, this object is missing or lacks key sub-properties that Chrome's browser extension system normally populates.AudioContext.state returns "suspended" immediately on creation in headless environments. Real browsers start in "running" state when user interaction has occurred.Notification.permission returns "denied" immediately without any user action โ because there's no UI to prompt the user. Real browsers start at "default" and only change after a permission request.navigator.languages array or a language that doesn't match the navigator.language single value. Real browsers always have a consistent, non-empty languages list.The Code: Basic Bot Detection Checks
function detectBot() {
const signals = [];
// 1. WebDriver flag
if (navigator.webdriver) {
signals.push('webdriver:true');
}
// 2. Headless Chrome: window.chrome check
if (!window.chrome || !window.chrome.runtime) {
signals.push('chrome_runtime:missing');
}
// 3. Empty plugins (headless indicator)
if (navigator.plugins.length === 0) {
signals.push('plugins:empty');
}
// 4. Software GPU renderer
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
if (gl) {
const ext = gl.getExtension('WEBGL_debug_renderer_info');
if (ext) {
const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
if (/SwiftShader|Mesa|llvmpipe|ANGLE/i.test(renderer)) {
signals.push('gpu:software_renderer');
}
}
}
// 5. Notification permission auto-denied
if (window.Notification && Notification.permission === 'denied') {
signals.push('notifications:auto_denied');
}
// 6. Languages inconsistency
if (!navigator.languages || navigator.languages.length === 0) {
signals.push('languages:empty');
}
return {
isBot: signals.length >= 2,
confidence: signals.length / 6,
signals
};
}
const result = detectBot();
console.log(result);
// Real browser: { isBot: false, confidence: 0, signals: [] }
// Headless bot: { isBot: true, confidence: 0.67, signals: ['webdriver:true', 'plugins:empty', 'gpu:software_renderer', ...] }
Framework-Specific Detection
| Framework | Primary Signals | Patches Used |
|---|---|---|
| Puppeteer | webdriver, SwiftShader GPU, empty plugins | puppeteer-extra-plugin-stealth patches most |
| Playwright | webdriver, missing chrome.runtime, notifications:denied | Chromium mode: fewer patches available |
| Selenium | webdriver:true, cdc_ variables in DOM, __$webdriver_evaluate | Older โ harder to fully patch |
| PhantomJS | window.callPhantom, window._phantom, outdated UA | Largely obsolete, easy to detect |
| Custom headless | SwiftShader GPU most reliable, behavioral patterns | Varies by implementation |
Why Behavioral Signals Beat Static Checks
Sophisticated bots patch the obvious static signals. navigator.webdriver can be set
to false. The plugin array can be spoofed. The chrome.runtime object can be mocked.
But behavioral signals are much harder to fake:
- Mouse movement entropy โ bots move in straight lines or not at all; humans are chaotic
- Typing cadence โ bots type at constant speed; humans have variable inter-key timing
- Scroll behavior โ bots scroll in discrete jumps; humans use smooth, variable scrolling
- CPU benchmark timing โ JS execution speed on a headless container differs from physical hardware
- Time-to-first-interaction โ bots often interact immediately; humans have variable load-to-click times
UNDETECT.CLUB combines static fingerprint checks with CPU timing benchmarks to assess whether the browser is running on real hardware or a virtualized/containerized environment.
Frequently Asked Questions
Check If Your Browser Looks Like a Bot
UNDETECT.CLUB runs full bot detection including webdriver flag, GPU renderer, plugin checks, and VM detection.
[ RUN BOT CHECK ]