Back to blog
·4 min read

Bot Detection Evasion in 2026: What Works and What Doesn't

Bot detection has evolved significantly. Hiding navigator.webdriver and faking a User-Agent barely scratches the surface of modern detection. Here's a breakdown of common evasion techniques and what they actually address.

Technique 1: User-Agent spoofing

Changes the navigator.userAgent string to claim a different browser or version.

await page.setUserAgent(
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
    '(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36',
)

The problem: Detection systems don't trust the User-Agent string. They verify it against JavaScript and CSS feature support, WebGL capabilities, and platform-specific behaviors. If your UA claims Chrome 147 but your browser's feature set doesn't match that version, the inconsistency itself becomes a detection signal.

Ineffective alone. Only useful when combined with a complete, consistent set of signals.

Technique 2: Stealth plugins

puppeteer-extra-plugin-stealth, playwright-stealth, and similar tools patch known detection vectors: navigator.webdriver, navigator.plugins, chrome.runtime, Permissions.query, and WebGL renderer strings.

What they don't fix:

  • Property descriptor analysis: is navigator.webdriver truly absent, or just patched to return false?
  • Prototype chain validation: is navigator.plugins a real PluginArray with the correct prototype?
  • Cross-scope consistency: do Workers see the same patched values?
  • Function.prototype.toString integrity: do patched functions still report [native code]?

A useful baseline for the most basic checks, but modern detection has moved past these signals.

Technique 3: Browser binary patching

Tools like Patchright (a Playwright fork) and undetected-chromedriver modify the browser binary itself rather than injecting JavaScript patches at runtime.

Why it's better: Binary-level patches survive iframe creation, Worker spawning, and toString() inspection because the modifications exist at the C++ level. Runtime JavaScript patches don't survive these checks.

What it doesn't fix: Binary patching removes automation artifacts, but doesn't create a consistent identity. Your fonts, GPU parameters, timezone, screen size, and API feature set still need to match a plausible real browser profile.

Technique 4: Real browser profiles

Launching with a persistent user data directory that has authentic browser state: cookies, history, extensions, cached data, stored permissions.

// Playwright example
const context = await chromium.launchPersistentContext(
    '/path/to/real-chrome-profile',
    {
        channel: 'chrome', // Use system Chrome, not Chromium
        locale: 'en-US',
        timezoneId: 'America/New_York',
    },
)

A real profile has authentic extension data, plugin state, and cached resources. The browser state is genuine rather than synthesized. The trade-off is operational complexity: maintaining and syncing real profiles adds work.

Technique 5: Headful mode with virtual display

Running Chrome in headed mode with xvfb (X virtual framebuffer) on Linux:

xvfb-run --auto-servernum chromium --no-sandbox

Old headless mode (Chrome's original --headless flag, before Chrome 112) had differences in font rendering, compositor behavior, and WebGL output. New headless mode (--headless=new, the default in modern Puppeteer/Playwright) uses the same rendering pipeline as headed mode, largely eliminating these differences. Using xvfb is still useful if you need to run older Chrome versions or want to ensure identical rendering behavior.

Still has automation artifacts from the control protocol (CDP/WebDriver). Best combined with binary patching.

What detection systems look at

Detection works in layers:

Layer 1: Automation flags

navigator.webdriver, window.chrome, CDP artifacts. Most bots patch these, so they're low-value signals on their own.

Layer 2: Fingerprint consistency

Cross-referencing browser signals for internal consistency. User-Agent version vs. feature support, claimed OS vs. font list, timezone vs. locale. This is where most bots fail.

Layer 3: Behavioral analysis

Mouse movements, scroll patterns, click timing, typing cadence. Analyzed server-side, outside the scope of fingerprint-level detection.

Layer 4: Network analysis

TLS fingerprint (JA3/JA4), HTTP/2 settings, TCP parameters. These reveal the actual network client regardless of JavaScript-level spoofing.

Layer 5: Reputation

IP reputation, ASN history, cookie age, account age. Historical signals that can't be fabricated in a single session.

StealthCheck focuses on layers 1 and 2, the signals visible through the browser's JavaScript environment. Layers 3 through 5 require different tools.

How to evaluate your setup

The key question isn't "which stealth plugin should I use?" but "what does my browser fingerprint look like?"

  1. Run a free test to see your current score
  2. Review the issues, each one explains what's detectable and why
  3. Fix the flagged issues and retest
  4. Retest after any Puppeteer/Playwright/Chrome update

Further reading

Test your browser stealth for free

Run a free fingerprint test and see exactly what detection systems see.