Puppeteer Stealth Not Working: Where to Look First
Puppeteer stealth not working almost always traces to one of three causes: the target now reads a CDP artifact the plugin never hid, your 2.11.2 stealth bundle no longer matches your Puppeteer version, or the connection itself is dirty — datacenter IP, brand-new profile, no history. The plugin only rewrites page JavaScript. Fix the Runtime.enable leak first, then version drift, then the network, and test after each change instead of all at once.
📌 TL;DR Executive Summary
- Core Takeaway: puppeteer-extra-plugin-stealth patches 17 JavaScript surface flags and nothing else. Modern detection also reads CDP artifacts, TLS fingerprints, IP reputation and session history, which the plugin cannot reach.
- Key Risk/Challenge: Patching harder makes things worse. Recreating chrome.runtime on current Chrome adds an artifact a genuine browser never exposes, and stacking evasion libraries adds tells instead of removing them.
- Recommended Solution: Close the Runtime.enable leak with rebrowser-patches or Patchright, pin Puppeteer and stealth versions together, then move fingerprint, IP and session persistence into isolated managed profiles.
What “Not Working” Looks Like in Practice
Stealth failures rarely announce themselves with the word “bot”. You get one of four outcomes, and each points somewhere different.
- A hard block. A 403, a Cloudflare interstitial that reloads in a loop, or a DataDome block page. Something read a signal it disliked before your page finished rendering.
- A challenge that never resolves. A Turnstile widget spins, then resets. The page loads, the verification does not.
- Degraded content. You receive HTML, but prices come back as placeholders or the account gets logged out mid-flow. That is scoring rather than blocking.
- Nothing at all. The request hangs, or returns instantly with an empty body.
Works on Your Laptop, Fails on the Server
This split is the most common false alarm in automation troubleshooting. Your laptop sends a residential-looking connection from the same country as the browser timezone, with a GPU the WebGL renderer can plausibly claim. A container on a cloud host sends a datacenter IP from another continent, a software rendering stack, and often a headless build.
The plugin behaves identically in both runs. The environment around it does not. Test the same script in both places against the same probe page before you touch your evasion setup.
Separate “My Setup Broke” From “the Target Tightened”
These two get conflated constantly, and the fix is different each time. If your script passed yesterday and fails today with no code change, inspect your dependency tree first: a fresh install can pull a Puppeteer release your stealth build was never tested against. If nothing moved in your repo, the site likely changed its rules.
Why It Happens: Four Layers, and Stealth Touches One
puppeteer-extra-plugin-stealth is a bundle of 17 evasion modules injected into pages before the site’s own scripts run. They patch navigator.webdriver, the missing window.chrome object, navigator.plugins and navigator.languages, the WebGL vendor string, and the sourceURL marker that betrays evaluated code. That is the complete job description. It does not touch your TLS handshake, your IP reputation, or the way you move a mouse.
Layer 1: The Runtime.enable CDP Leak
Puppeteer speaks the Chrome DevTools Protocol, and to evaluate JavaScript in a page it enables the Runtime domain. Enabling that domain has an observable side effect on the page side, so a script running in the page can tell a debugger-style client attached — without reading any flag stealth already hides. DataDome and Cloudflare both rely on this Runtime.enable leak.
No combination of the 17 modules in the 2.11.2 bundle closes it, because the leak is not a JavaScript property. It is a consequence of the client attached to the page.
Layer 2: Environment Inconsistencies
Fingerprint checks do not hunt for one bad value. They hunt for values that cannot coexist. A macOS user agent while User-Agent Client Hints still report Windows is a contradiction. A WebGL renderer naming an Apple GPU inside a Linux container is a second. navigator.languages that disagree with the proxy exit country is a third.
Any one of those is weak evidence alone. A set of them reads as a manufactured identity.
Layer 3: Network Fingerprint and IP Reputation
Stealth runs inside the page. Your TLS ClientHello leaves before any JavaScript exists, so nothing the plugin does can change it. Cloudflare now exposes JA4 fingerprints and inter-request JA4 Signals in Firewall Rules, Bot Analytics and Workers, analysing over 15 million unique JA4 fingerprints a day from a dataset built on more than 500 million user agents. A TLS profile that does not match a browser claiming to be Chrome is visible before your first request completes. The JA3 fingerprinting explained walkthrough covers the handshake side.
IP reputation compounds it. undetected-chromedriver has exactly this gap: it patches the driver and leaves your address untouched, so datacenter exits still fail with a clean driver. A large share of “stealth stopped working” reports are network problems wearing a fingerprint costume.
Layer 4: Session History and Behaviour
A profile with no cookies, no local storage and no history is a statistical anomaly many systems score. Real visitors arrive with a cached consent cookie, a stored language preference, a live session token. Starting from a clean profile on every run produces that anomaly on every run, which turns it into a pattern.
Behaviour sits on top: pointer movement, scrolling, timing. Fixed sleeps and instant clicks are as legible as a missing property. A form submitted the instant its fields render is not describing a person, and no evasion module changes that.
Quick Wins: Six Fixes You Can Run Today
- Pin the pair. Lock Puppeteer to a minor version you tested and commit the lockfile. A caret range is how a working script breaks overnight with an unchanged repository.
- Stop overriding the user agent. Once you set a UA string you own every consequence of it, including Client Hints, platform and WebGL coherence. Deleting the custom UA is often a net win.
- Run a probe before every change. bot.sannysoft.com covers the classic headless signals; the rebrowser-bot-detector page checks the modern ones. Keep both in your test suite.
- Align timezone, locale and languages with the exit IP. A proxy in Frankfurt with a browser set to America/New_York is a mismatch you chose yourself.
- Reuse a profile instead of starting clean. Persist cookies and storage between runs; continuity is cheap and removes a scoring dimension.
- Leave datacenter addresses for anything that matters. Residential or mobile exits change the calculus more than any plugin update.
Closing the Runtime.enable Leak
rebrowser-patches is a set of Node patches for Puppeteer and Playwright that disables the automatic Runtime.enable on every frame and creates execution contexts manually with unknown IDs instead. Main-world access keeps working, and the approach handles web workers and iframes. The project also publishes drop-in packages — rebrowser-puppeteer, rebrowser-puppeteer-core, rebrowser-playwright and rebrowser-playwright-core — which you can alias in package.json without editing your source.
Four runtime fix modes are set through the REBROWSER_PATCHES_RUNTIME_FIX_MODE environment variable: addBinding (the default), alwaysIsolated, enableDisable, and 0 to switch the fix off entirely. Start with the default; change mode only when a target’s own scripts misbehave inside the isolated context.
// package.json — alias the patched package, no source changes needed
// "dependencies": {
// "puppeteer": "npm:rebrowser-puppeteer@latest"
// }
// Runtime fix mode: addBinding (default) | alwaysIsolated | enableDisable | 0 (off)
process.env.REBROWSER_PATCHES_RUNTIME_FIX_MODE = 'addBinding';
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: false,
channel: 'chrome' // real Chrome build, run headful
});
const [page] = await browser.pages();
await page.goto('https://bot.sannysoft.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'probe.png', fullPage: true });
await browser.close();
})();
Then re-run the project’s own detector page: rebrowser-bot-detector tests the current generation of automation leaks rather than the ones stealth 2.11.2 was written for. The patch set and its mode documentation live on the rebrowser-patches repository.
Version Drift: Why a Fresh npm install Breaks a Working Script
puppeteer-extra-plugin-stealth has sat on version 2.11.2 for a long time. The berstend/puppeteer-extra monorepo last received a push on 18 July 2024 and carried 275 open issues as of late August 2026, while the package still pulled 4,441,820 npm downloads in the month to 28 August 2026. It is not formally deprecated and it has not been removed from npm. It simply is not tracking Puppeteer any more.
Puppeteer has moved on to the 25.x line. Pairing a current Puppeteer with the older stealth bundle can throw “Cannot read properties of undefined” from individual evasions, because the internal APIs those modules reach into changed shape. That failure is at least loud. The quieter version is a module that silently no-ops and leaves a flag exposed with no error at all.
Pin Puppeteer to a specific minor version, install with npm ci so the lockfile is authoritative, and re-run your probe suite after every dependency bump — before shipping anything. If you want to know which module owns which signal before you change versions, this breakdown of the stealth plugin’s evasion modules maps each one to the check it answers.
Alternatives: What Each One Actually Fixes
Swapping stacks without knowing which layer you are buying is how people end up three libraries deep and still blocked.
| Tool | Fixes | Does not fix | Fits when |
|---|---|---|---|
| puppeteer-extra-plugin-stealth 2.11.2 | JS surface flags: webdriver, window.chrome, plugins, languages, WebGL vendor, sourceURL | Runtime.enable, TLS fingerprint, IP reputation, behaviour | Light bot checks, low consequences |
| rebrowser-patches (drop-in packages) | The Runtime.enable leak across frames, workers and iframes | TLS, IP reputation, session history | Puppeteer or Playwright code you cannot rewrite |
| Patchright | Runtime.enable via isolated contexts, Console.enable, default launch args — Chromium only | Non-Chromium engines, IP reputation, the console output you debug with | Playwright users on Chromium-visible targets |
| undetected-chromedriver | ChromeDriver automation flags for Selenium stacks | Your IP — datacenter exits still fail | Legacy Selenium with no reason to migrate |
| nodriver | A newer Python route positioned as the successor to the undetected-chromedriver pattern | The same network limits | Python projects moving off ChromeDriver |
| puppeteer-real-browser | Extra handling aimed at stronger bot checks | Fingerprint and proxy management | Quick experiments you can afford to iterate on |
What Patchright Changes, and What It Costs You
Patchright avoids Runtime.enable by running JavaScript in isolated execution contexts, and it patches the Console.enable leak by disabling the Console API entirely — which means page console output stops working, so do not plan on it for debugging. It also rewrites Playwright’s default launch arguments: it adds –disable-blink-features=AutomationControlled and removes –enable-automation, –disable-popup-blocking, –disable-component-update, –disable-default-apps and –disable-extensions.
Two constraints shape where it fits. It patches Chromium-based browsers only; Firefox and WebKit are not supported. Its recommended setup is a persistent context running the real Chrome channel headful, with no custom user agent or headers — itself an argument for not setting those in your script. Its InitScripts are injected through Playwright Routes, which the project acknowledges could in theory be detected by timing attacks, though it states no anti-bot vendor checks for this today. That is the project’s own position, not an independent measurement, and the same caution applies to its published pass list. Patchright vs Playwright stealth breaks the trade-offs down further.
Stopping It Coming Back
Recurring failures usually come from an unpinned stack and an untested environment, not from a clever new detection rule.
- Keep the probe suite in CI. A nightly run against the public detectors surfaces a regression before a client does.
- Enable only the CDP domains you need. Every domain your client switches on adds page-side surface; audit what your script actually calls and turn the rest off.
- Re-check the target in plain Chrome. If a normal browser loads it fine, the target did not change — your setup did.
- Change one variable per run. Two changes at once and you learn nothing about which one worked.
Automate Puppeteer Stealth Not Working With Send.win
Send.win pairs isolated, fingerprint-managed browser profiles with a full Automation API, so your scripts run in profiles that look and behave like real, separate users:
- Selenium, Puppeteer & Playwright support – drive any profile programmatically (Team plan)
- Isolated profiles – each with its own fingerprint, cookies, and storage
- Built-in residential proxies – with automatic timezone, locale, and WebRTC matching
- Desktop app for Windows, macOS & Linux – plus cloud sessions when you don’t want a local install
Try the instant cloud browser demo — no install, straight from your browser. Then compare plans: a 30-day free trial with no credit card, and paid plans from $6.99/month billed annually.
When to Stop Patching and Isolate the Profile Instead
Count your last twenty failures by cause. If most were IP reputation, a clean profile every run, or a GPU and timezone that never matched the claimed OS, more evasion code will not help — none of those live in page JavaScript.
That is the point where the browser identity belongs in a managed profile rather than in your script. Sendwin Browser is a desktop app for Windows, macOS and Linux built on a patched-Chromium engine with the Sendwin Stealth engine built in. It spoofs canvas, WebGL, audio, fonts and hardware at the engine level instead of through script injection, and keeps those values coherent so no two profiles share a fingerprint. Every plan includes built-in residential proxies, and timezone, locale, WebRTC and geolocation follow the proxy exit IP automatically. Profiles persist, so cookies and logins survive between runs — which removes the fresh-profile anomaly that has nothing to do with your code.
For a script, the useful part is that you stop launching a browser inside your automation process. The Team plan includes the local Automation API for Selenium, Puppeteer and Playwright: you take the CDP URL from a profile’s automation settings and attach your existing script to a browser that already looks like a normal machine, on an address that matches its stated location.
from playwright.sync_api import sync_playwright
CDP_URL = "http://127.0.0.1:PORT" # copy it from the profile's automation settings
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(CDP_URL)
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://bot.sannysoft.com", wait_until="domcontentloaded")
print(page.title())
browser.close()
Your client-side fixes still apply — the Runtime patch and the version pinning travel with your code either way. What moves out of your code is fingerprint consistency, IP reputation and session history, which is where most of these failures came from. If you would rather install nothing locally, the cloud browser runs the same profiles on EU and US nodes from any device, with a free 10-minute daily preview. The isolated profiles for automation guide covers the wiring end to end.
🏆 Send.win Verdict
Most puppeteer stealth not working threads end with another evasion module. The failures usually sit one layer down: a Runtime.enable artifact, a stale plugin against a newer Puppeteer, or a datacenter IP with a spotless profile attached. Sendwin Browser covers the layer your script cannot reach — engine-level canvas, WebGL, audio, font and hardware spoofing kept coherent per profile, residential proxies included on every plan, and timezone, locale and geolocation following the exit IP. Profiles persist and sync across devices, so your automation stops arriving as a first-time visitor on every run.
Try Send.win free today — start the 30-day desktop trial at $0, keep your local profiles on your machine, and on Team attach your Puppeteer or Playwright script to a profile CDP URL instead of launching your own browser.
Frequently Asked Questions
Why did Puppeteer stealth suddenly stop working?
Two causes dominate: your dependencies moved, or the target’s rules moved. A fresh install can pull a Puppeteer release the 2.11.2 bundle was never tested against, which shows up as a thrown error or a module that quietly stops patching. If nothing in your repo changed, assume the site tightened and test against a probe page first.
Does puppeteer-extra-plugin-stealth still work in 2026?
On simple targets, yes. It still handles the classic flags — navigator.webdriver, window.chrome, navigator.plugins, WebGL vendor — and still pulls millions of installs a month. Against Cloudflare, DataDome and Kasada-style stacks it falls short, because those weigh CDP artifacts, TLS and IP reputation alongside page JavaScript.
How do I fix the Runtime.enable CDP leak?
Use rebrowser-patches, or one of its drop-in packages such as rebrowser-puppeteer aliased in package.json. It stops the automatic Runtime.enable on every frame and creates execution contexts manually with unknown IDs, keeping main-world access and working with workers and iframes. Set the mode through REBROWSER_PATCHES_RUNTIME_FIX_MODE and leave it on the addBinding default unless something breaks.
Why does stealth work on my machine but fail on a server?
The plugin is identical in both places; the environment is not. A server typically presents a datacenter IP, a country that disagrees with the browser timezone, a software WebGL renderer and often a headless build. Fix the environment before adding evasion modules — residential exits and a matching timezone usually matter more than another patch.
How do I test whether my setup is detected?
Run bot.sannysoft.com for the classic headless signals and rebrowser-bot-detector for the modern ones, then repeat the same test in a plain, unpatched Chrome. The differences between the two outputs are your real leaks. Change one variable at a time so you know what fixed it.
Can stealth bypass Cloudflare Turnstile?
Not reliably. Turnstile combines several dimensions rather than running one check, and Cloudflare surfaces JA4 fingerprints and inter-request JA4 Signals across its analytics and rules, drawn from a dataset built on more than 500 million user agents. JavaScript patching cannot change your TLS handshake or your IP reputation, so there is a ceiling no plugin update lifts.
Is Patchright better than puppeteer-extra-plugin-stealth?
They solve different problems. Patchright is a patched Playwright for Chromium only, and it closes the Runtime.enable and Console.enable leaks — but it disables the Console API, so page console output stops working. If you are on Playwright with Chromium targets it is a cleaner fit; on Puppeteer, or with Firefox and WebKit, it is not an option.
What should I do when patching stops being enough?
Move the parts your script cannot control — fingerprint consistency, IP reputation, session history — into isolated profiles that persist between runs. Automation then attaches to a browser that already presents a coherent machine on a matching address, and your code goes back to doing the task instead of fighting detection.