Guide

puppeteer-extra-plugin-stealth: what it still covers in 2026

It is the most-installed anti-detection package for Node and it solves a narrower problem than most people assume. Here is what it patches, what it never touched, and how to tell whether it is your bottleneck.

Search for "how to stop Puppeteer getting blocked" and the same package comes up every time. puppeteer-extra-plugin-stealth has been the default answer for years, it has millions of downloads, and a large number of production pipelines depend on it.

It is also routinely asked to solve problems it was never designed for. Knowing which category your problem falls into saves a lot of time spent re-reading its README.

The shape of it

puppeteer-extra is a plugin wrapper around Puppeteer. The stealth plugin attaches to it and injects a set of evasions into every page before site scripts execute.

const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");

puppeteer.use(StealthPlugin());

const browser = await puppeteer.launch({ headless: "new" });

The evasions are grouped internally as separate modules, each targeting one inconsistency, which is useful when you need to know precisely what is being changed.

What it patches

The evasions fall into a few families.

Automation markers. The headline one is navigator.webdriver, which is true in an unmodified automation browser and undefined in a real one. Also handled: the Chrome DevTools connection being visible in ways a normal page does not expose.

Browser surface completeness. navigator.plugins and navigator.mimeTypes are empty under automation and populated in real Chrome. The plugin fabricates a plausible set. Similarly for navigator.languages, navigator.platform, and the chrome.runtime object that sites check for to confirm a genuine Chrome context.

Rendering and hardware. WebGL vendor and renderer strings, and the UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL extensions from WEBGL_debug_renderer_info. Without patching these, a headless browser frequently reports SwiftShader or a generic software renderer, which is a strong signal.

Media and codec surfaces. Codec support probed via canPlayType, and audio context behaviour, which differ between headless and real browsers.

Iframe consistency. Properties read through an iframe's contentWindow, which is a classic way to detect that a property has been overridden only on the top-level window. This is a subtle one, and its presence is a sign the package has been maintained rather than abandoned.

Native function integrity. Function.prototype.toString leaking that a patched function is not native code, and related checks.

What it never touched

None of the following are failures of the package. They sit outside the layer it operates in.

TLS fingerprints. The handshake carries a signature: ciphers, extensions, and their ordering, hashed as JA3 or JA4. It is negotiated before any JavaScript runs. A server that scores on this has already decided before the plugin's first line executes.

HTTP/2 fingerprints. Frame settings, header ordering, and priority frames. Also outside the page.

IP reputation. Datacenter ranges are catalogued and scored. Installing a plugin does not change which ASN your packets come from.

CDP artefacts. Puppeteer drives the browser over the Chrome DevTools Protocol. Detection of that protocol connection is a well-developed technique, and it is not addressed by overriding navigator properties.

Behaviour. Request cadence, navigation timing, and interaction patterns. A perfectly patched browser that behaves like a for loop still scores like one.

The pattern to internalise: the package operates inside the page. Every signal listed above is measured outside it. That boundary explains almost every "the stealth plugin stopped working" report.

The modern headless problem

Puppeteer's newer headless mode is much closer to a real browser than the original headless implementation, which was a separate rendering path with obvious differences. That closed a class of detection.

It did not close the classes above, and it introduced a subtlety: running headless at all changes some observable behaviour, and the plugin's evasions are calibrated against specific expectations. Upgrading Puppeteer can therefore change which evasions matter, and a pipeline that worked on an older version can regress on a newer one without any code change.

Pin your Puppeteer version and treat upgrades as a change that needs re-verification against your targets.

How to tell if it is your bottleneck

Run these before you touch the plugin's configuration, because the answer is usually no.

Does it fail from a residential connection too? If a home connection succeeds with identical code, your problem is the ASN. No plugin will fix it.

Does a real Chrome on the same machine succeed? If it does and Puppeteer does not, something in the automation stack is being detected, and it may well be TLS or CDP rather than a navigator property.

Does adding a delay fix it? If yes, you were rate limited, not detected. The most common cause of "detection" at modest volume is request pacing.

Is it always blocked, or sometimes? Consistent failure suggests a hard fingerprint signal. Intermittent failure suggests scoring, rate limits, or a challenge that sometimes passes.

What does the response say? A 403 with a challenge body, a 200 with an empty body, and a 429 mean three different things. Log the status and the body rather than just the exception.

What to do instead

If you have established that the block is not something the plugin covers, the reachable options are the familiar ones.

Slow down and cache. Free, and it resolves more reported problems than any package. If the content is stable between runs, caching removes the request entirely.

Use persistent sessions. A browser context that keeps cookies and storage across requests presents as a returning visitor. This is a meaningful signal and it costs nothing.

Route through residential or static ISP addresses. Addresses IP reputation, which is usually the largest single contributor. Static ISP often outperforms rotating residential because a session stays on one clean address.

Accept the maintenance, or stop doing it yourself. A self-managed stack that survives current defences needs the browser's TLS and HTTP/2 fingerprints to match a shipping Chrome release, plus CDP detection avoidance, plus per-target tuning. That is a system with an owner. If page fetching is incidental to your product, paying per page for validated output is usually the cheaper answer, and the maintenance cost is the reason.

Everything here concerns publicly accessible pages. Respect robots.txt, keep request rates courteous, and do not aim any of it at authentication walls or paywalls. See our acceptable use policy.

datamint returns schema-validated JSON and LLM-ready Markdown from bot-defended sites through one API call, with routing across transport tiers handled on our side.

All guides