Blog/Product
12 min read

Puppeteer Extra Plugin Stealth: Check the Release Date First

The stealth plugin last shipped in March 2023. Puppeteer shipped last week. What that gap means, and what scrapy-playwright does differently.

Puppeteer Extra Plugin Stealth: Check the Release Date First

Copy this line to your agent to render a protected page without maintaining an evasion stack.

set up https://monid.ai/SKILL.md and use context.dev /web/scrape/html to render a page that needs a real browser

On 2026-09-01 the npm registry says puppeteer-extra-plugin-stealth last shipped version 2.11.2 on 2023-03-01. Puppeteer itself shipped 25.9.0 on 2026-08-25. The layer whose entire job is keeping up with detection has not moved in three and a half years, while the browser it patches ships every few weeks. That gap is the most useful fact about this package, it is checkable in one command, and this guide is about what follows from it, running through Monid, the OpenRouter for agent tools.

Does puppeteer-extra-plugin-stealth still work?

Partly, in a way that is worse than either yes or no.

What the registry says

Read on 2026-09-01, straight from the npm and PyPI registries:

PackageLatestPublished
puppeteer25.9.02026-08-25
puppeteer-extra3.3.62023-03-01
puppeteer-extra-plugin-stealth2.11.22023-03-01
rebrowser-puppeteer-core24.8.12025-05-09
puppeteer-real-browser1.4.42025-09-03
scrapy-playwright0.0.482026-07-10

Two things stand out. The stealth plugin and its host framework froze on the same day, which is what an abandoned project looks like rather than a stable one. And the browser they wrap has moved several major versions since.

What still works

The easy evasions. navigator.webdriver, the plugins array, the languages array, the chrome object: these are old signals that the plugin patches, and patching them still removes those specific tells. If a site is checking only the obvious properties, the plugin is fine.

What does not

Anything added to detection after early 2023. Behavioural signals, CDP-level artifacts, execution-context leaks, and the fingerprinting techniques that commercial anti-bot vendors have shipped in three years of active development. The plugin cannot patch what it was never told about.

Why "partly" is the bad answer

If it failed outright you would know on the first run and choose something else. Instead it works on your test site, works in development, works against the two pages you checked, and then a fraction of your production targets return a challenge page with HTTP 200. That is the same silent-failure shape documented in the Amazon ASIN guide and in what a blocked scraper actually returns, and it is why the release date is worth checking before you build on top of it.

📖 See also Node Unblocker: What It Does Not Do

Why does an unmaintained evasion layer fail quietly?

Because detection is adversarial and the defender publishes nothing.

The asymmetry

An anti-bot vendor ships a new signal on Tuesday and tells no one. There is no changelog, no deprecation notice, no error message that says your fingerprint is wrong. From your side, the page just comes back different: shorter, or with a challenge, or with the content missing. Nothing throws.

What that means for a frozen package

A stealth plugin is a snapshot of what detection looked like on its release date. It does not degrade gracefully, it degrades invisibly, and the rate at which it degrades is set entirely by how fast the other side moves. Three and a half years is a long time in that race.

The measurable version of this

You cannot test evasion by testing your code, only by testing against targets. So the practical check is a canary: run the same known-good URL on a schedule, assert on a string that must appear in a correctly fetched page, and alert on the rate rather than on individual failures.

CANARY = "https://example.com/a-page-you-know"
MUST_CONTAIN = "Some text only the real page has"

def check(html):
    return MUST_CONTAIN in html   # not len(html) > 0, not status == 200

Length checks pass on challenge pages. Status checks pass on challenge pages. A content assertion does not.

The forks, briefly

rebrowser-puppeteer-core and puppeteer-real-browser both exist because of the gap above, and both have shipped more recently, in May 2025 and September 2025 respectively. They are real improvements over a 2023 snapshot. They are also, on 2026-09-01, themselves a year old, which puts them in the same category one notch back. The structural point survives the specific packages: an evasion layer that is not being actively maintained against live targets is a depreciating asset, and you inherit its maintenance the moment you depend on it.

How do you render a protected page without maintaining evasions?

Three steps. The first is free.

For agents

Grab an API key at app.monid.ai, then paste this to your agent and hand it the key:

set up https://monid.ai/SKILL.md

It learns the whole discover, inspect, run workflow itself. More in the agent quickstart.

For humans

npm install -g @monid-ai/cli
monid keys add -k <your-api-key> -l main

Step 1. See what a hosted browser returns for your target

What it does. Renders the page in someone else's maintained browser and hands you the HTML.

The endpoints. context.dev/web/scrape/html, billed per call.

The call.

monid run -p context.dev -e /web/scrape/html --query '{"url": "https://bot.sannysoft.com/"}'

What comes back. Pointed at a public fingerprint test page on 2026-09-01, 42KB of rendered HTML reporting navigator.webdriver as "missing (passed)", the advanced WebDriver check passed, a plugins array of length 5 with the correct PluginArray type, and a user agent of Chrome/152.0.0.0 on Linux.

It also reported "Canvas has no webgl context" for both WebGL vendor and renderer, which is a fail on that test and worth stating plainly. A hosted renderer is a maintained browser, not a perfect human. On sites that fingerprint WebGL specifically, that is a signal.

What it costs. A fraction of a cent per call. Current figures at monid.ai/tools.

Step 2. Take Markdown when you want the content, not the DOM

What it does. Skips the parsing step entirely when what you actually wanted was the text.

The endpoints. context.dev/web/scrape/markdown, billed per call.

The call.

monid run -p context.dev -e /web/scrape/markdown --query '{"url": "https://example.com/article"}'

What comes back. Clean Markdown plus metadata. Most scraping projects that reach for a headless browser wanted readable content, and the browser was a means to that end. If that is you, this removes both the browser and the selectors.

What it costs. Same order as above.

Step 3. Assert on content, not on the response

What it does. The step that catches the failure mode this whole article is about.

The call. No endpoint:

def usable(html: str) -> bool:
    if len(html) < 2000:            # challenge pages are small
        return False
    if "captcha" in html[:4000].lower():
        return False
    return MUST_CONTAIN in html     # the real check

What comes back. A block rate you can watch. Whether you run your own browser or call a hosted one, this is the check that tells you something changed, and it is the one people leave out.

Give this to your agent

$Set up https://monid.ai/SKILL.md, and then use Monid to fetch these 40 URLs as markdown, and give me two lists: the ones containing the article body, and the ones that came back as a challenge page.

What does scrapy-playwright actually solve?

A different problem from the one above, and the distinction is worth being precise about because the two packages get recommended in the same breath.

It is an integration, not an evasion

scrapy-playwright at 0.0.48, released 2026-07-10 and requiring Python 3.10 or newer, connects Scrapy's scheduling machinery to a Playwright browser. It handles the awkward part: letting a Scrapy request opt into browser rendering by setting meta={"playwright": True}, while everything else in your spider, the middleware, the item pipelines, the retry logic, keeps working.

That is genuinely useful and it is actively maintained, which is why it appears in this article as a contrast rather than a warning.

What it does not give you

Stealth. A Playwright browser driven by scrapy-playwright is a Playwright browser, with whatever fingerprint that carries. If your target uses a commercial anti-bot service, the integration layer will faithfully deliver you a challenge page.

People conflate these because both live under "how do I scrape JavaScript sites in Python", and the answer splits: rendering and not getting blocked are separate problems with separate solutions, and only one of them is solved by an integration package.

The selective pattern that keeps it affordable

The reason to use scrapy-playwright rather than making everything a browser request is that browsers are expensive in memory and time. The pattern that works:

def parse(self, response):
    for url in response.css("a.item::attr(href)").getall():
        yield response.follow(url, self.parse_item)   # plain HTTP

def parse_item(self, response):
    if not response.css("div.price"):                 # only when needed
        yield response.request.replace(
            meta={"playwright": True}, dont_filter=True)

List pages are usually static. Detail pages are usually where the JavaScript lives. Rendering the whole crawl when a tenth of it needs a browser is the most common way a Scrapy project becomes slow.

Where a hosted renderer fits into Scrapy

It fits as a downloader for the pages that need it, which lets you keep Scrapy for what Scrapy is good at, scheduling, deduplication, retries and pipelines, without also running a browser fleet. That is the same reasoning as when to run your own crawler: the framework is worth keeping, the infrastructure underneath it often is not.

📖 See also Browser Automation Without an API

Which endpoint should I use for which job?

EndpointWhat it doesInputOutputBest forBilling
context.dev/web/scrape/htmlRenders and returns HTMLA URLFull rendered HTML plus metadataYou need the DOM and your own selectorsPer call
context.dev/web/scrape/markdownRenders and convertsA URLClean Markdown, metadata, JSON-LDYou wanted the content, not the DOMPer call
context.dev/web/searchSearch plus optional scrapeA queryResults, optionally with page contentYou do not have the URLs yetPer result
context.dev/brand/ai/productStructured product extractionA URLTyped product fieldsEcommerce pages specificallyPer call

Every row was verified with monid inspect on 2026-09-01. The table gives billing shape rather than figures; shape drives design and current numbers live on monid.ai/tools.

The third row is the one people miss. A surprising share of browser-automation projects begin with "I need to scrape these search results", and a search endpoint that optionally scrapes each result removes both the browser and the SERP parsing in one call.

When should you still run your own browser?

Four cases, and they are real.

You are automating, not extracting. Filling forms, clicking through a flow, driving an authenticated session, taking screenshots at specific viewports. A fetch endpoint returns a page; it does not operate one. Playwright and Puppeteer are the right tools and there is no hosted substitute.

You are logged in. Session state, cookies, and anything behind your own credentials belong in a browser you control, for both practical and policy reasons.

Volume makes per-call billing worse than infrastructure. There is a crossover point where running browsers is cheaper than paying per page. It is further out than most people assume once you count the engineering time this article is about, but it exists, and it is a legitimate reason to own the stack.

You are testing your own site. Then detection is not adversarial, stealth is irrelevant, and Playwright is simply the correct tool.

And the disclosure: this is Monid's blog and we sell the hosted alternative. Two things in this article cut against that, and both are there deliberately. The WebGL failure on the fingerprint test is a real weakness of the endpoint we are recommending. And the four cases above are cases where you should not buy from us. An article that omitted them would be an advertisement, and an advertisement would not have been worth checking the registry for.

Conclusion

puppeteer-extra-plugin-stealth last shipped on 2023-03-01. Puppeteer shipped on 2026-08-25. That single comparison tells you more about whether your stack will hold than any benchmark, because an evasion layer's value is entirely a function of how recently it was updated against live targets.

If you keep running it, add a content-level canary so you learn about degradation from your monitoring rather than from a quarter of bad data. If you are on Scrapy, scrapy-playwright is actively maintained and solves integration properly, but it is not a stealth layer and treating it as one is the mistake this article exists to prevent. And if the browser was only ever a way to get the content, a maintained hosted renderer removes a maintenance burden you did not choose to take on.

Free next step: run npm view puppeteer-extra-plugin-stealth time.modified and compare it against npm view puppeteer time.modified. It costs nothing, takes ten seconds, and it is the check nobody makes before adopting an evasion library. Start at monid.ai.

FAQ

Should you remove puppeteer-extra-plugin-stealth from an existing project?

Not reflexively. If your targets are not using modern anti-bot services and your success rate is good, it is doing a job and removing it will not improve anything. What you should do is add the content-level canary, so that the day your success rate starts sliding you find out from an alert rather than from a stakeholder asking why the numbers look odd. The dependency is not the risk; the absence of a signal is.

Are the maintained forks worth switching to?

They address a real gap. rebrowser-puppeteer-core patches CDP-level leaks that the original does not, and puppeteer-real-browser takes a different approach by driving a real browser installation. Both had 2025 releases against the original's 2023 one. The honest caveat is that on 2026-09-01 both are themselves a year old, so switching buys you a newer snapshot rather than an ongoing guarantee. Evaluate them against your actual targets, because that is the only test that means anything here.

Is scrapy-playwright better than scrapy-splash?

For new work, yes, and the reason is maintenance rather than architecture. Splash runs a separate service on an older browser engine; scrapy-playwright drives current Chromium, Firefox or WebKit in process and shipped in July 2026. The Splash pattern of a separate rendering service is not wrong, and it is essentially what a hosted rendering endpoint is, just with someone else keeping the browser current.

How many concurrent browsers can you actually run?

Far fewer than you expect, and this is the cost that surprises people. Each Chromium context wants hundreds of megabytes, so a machine that comfortably handles hundreds of concurrent HTTP requests handles perhaps a dozen browsers. Set PLAYWRIGHT_MAX_CONTEXTS and Scrapy's concurrency to numbers your memory can actually support, and route only the pages that need rendering through the browser. This arithmetic, more than any blocking issue, is what usually decides whether to host browsers or call someone else's, and the real cost of scraping YouTube yourself puts numbers on it.

Last updated September 2026.

puppeteer-extra-plugin-stealthscrapy-playwrightheadless browserbot detectionbrowser automation