How to Use Stealth Mode in Headless Browsers
Use Puppeteer Extra Stealth for authorized browser testing, choose the right headless mode, and understand why no setup guarantees an undetectable browser.
For authorized QA, development, or research, the documented way to add stealth behavior to a Puppeteer script is to register puppeteer-extra-plugin-stealth with Puppeteer Extra before launching the browser. The plugin adjusts multiple browser-visible signals, but it does not make a headless browser undetectable or guarantee access to any site.
This guide shows the documented Puppeteer setup, a Playwright Extra variant, how to choose a headless mode for your test, and how to diagnose common setup problems. Use automation only on systems you own or are allowed to test. Puppeteer’s security policy places responsibility for safe and intended use on the calling code: Puppeteer security policy.
1. What “stealth mode” means
There is no single universal stealth switch in the browser standards or in the sources covered here. In this guide, stealth mode means using a browser automation plugin that changes a collection of browser-visible indicators that some detection systems inspect.
The best-documented example in this article is puppeteer-extra-plugin-stealth. It is used through Puppeteer Extra and can also be registered with Playwright Extra. The project describes its techniques as modular. Its maintainer also acknowledges that headless Chromium can still be detected; the plugin is an attempt to reduce some detectable signals, not a promise that automation will pass a particular check. See the project README.
2. Install and use Puppeteer Extra Stealth
The documented flow is: install the packages, register the plugin, launch Puppeteer, then use the normal page API. The example below navigates to a public browser test page and saves a screenshot so you can verify that the script runs; it does not prove that a real site will accept automation.
npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
// stealth-check.js
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
async function main() {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('Title:', await page.title());
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Run it with node stealth-check.js. Replace the example URL with a staging or test site you are authorized to automate. The package README also documents an ES module / TypeScript-style import pattern:
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
puppeteer.use(StealthPlugin());
Register the plugin before calling launch(). If you configure the plugin’s optional modules, consult the README for the package version you have installed; avoid copying a recipe that was written for a different version without checking its current options.
3. Use the plugin with Playwright Extra
The project documents registering the stealth plugin with the Chromium instance from playwright-extra. This is a Chromium-focused compatibility claim from the package documentation, not a claim that the same behavior applies to every Playwright engine.
npm install playwright playwright-extra puppeteer-extra-plugin-stealth
// playwright-stealth-check.js
const { chromium } = require('playwright-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
chromium.use(StealthPlugin());
async function main() {
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('Title:', await page.title());
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Check the current Playwright Extra package guidance and installed versions before relying on this integration. Playwright supports Chromium, Firefox, and WebKit, but the stealth plugin describes itself as optimized for Chromium-based browsers. Do not assume equivalent behavior across engines.
4. Choose the browser mode that matches the test
Stealth configuration and headless mode are separate decisions. Pick the browser mode that matches what you are trying to validate; a more browser-like mode is not evidence that a site will accept automation.
| Configuration | Useful when | Tradeoff to check |
|---|---|---|
| Puppeteer default headless mode | You want the browser mode Puppeteer currently uses by default for automation. | Puppeteer distinguishes this from the separately shipped chrome-headless-shell; verify behavior against your installed Puppeteer version. |
chrome-headless-shell |
Your automation does not need the full Chrome feature set and the shell fits the target environment. | Puppeteer says it does not completely match regular Chrome, though it can be more performant for tasks that do not need the complete feature set. |
| Playwright headless shell | You want Playwright’s default headless setup. | Playwright ships a separate Chromium headless shell for headless operation; validate that it matches the test target. |
| Playwright new headless mode | You want to test with the newer headless mode documented for the Chromium channel. | Playwright documents opting in with the chromium channel. Confirm the browser is installed and the mode is appropriate for your test. |
| Headful execution | You need to reproduce a headed user-facing workflow or debug visually. | It requires a display environment unless your runner provides one; use headless: false in Puppeteer. |
See the browser projects’ current documentation for details: Puppeteer headless modes and Playwright browsers. Playwright supports Chromium, Firefox, WebKit, branded Chrome and Edge channels, and device configurations. Keep the framework and browser versions aligned with the environment you mean to test.
5. Validate a stealth setup responsibly
- Use a permitted target. Start with your own application, staging environment, or a test page whose owner allows automated access.
- Record the environment. Note the Node.js version, framework and plugin versions, browser engine, browser channel, and headless/headful mode.
- Run the same test with and without the plugin. Compare only the behavior relevant to your authorized test. A difference on one test page does not predict behavior on another site.
- Check the actual application outcome. Look for navigation, page content, console errors, and expected test assertions. A successful browser launch is not proof that a remote service permits automation.
- Recheck after upgrades. Browser and package behavior can change. Playwright recommends keeping it updated; review the plugin’s current compatibility notes too.
The stealth project README says its authors believe there are still ways to detect headless Chromium and describes detection as an ongoing cat-and-mouse problem. Its statement about public bot tests is a maintainer claim, not an independent benchmark or a guarantee for your target. Do not treat any plugin setting as a way around a site’s access rules.
6. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
Cannot find module 'puppeteer-extra' or plugin import fails |
The package was not installed in the project where the script runs, or the module syntax does not match the project configuration. | Run the install command in the project directory. Use CommonJS require as shown, or configure ES modules and use the README’s import pattern. |
| Browser fails to launch | Browser binaries are missing, incompatible with the installed framework, or unavailable in the runtime environment. | Install the browser expected by your framework, check the launch error and framework version, and try a local minimal launch without the plugin to isolate the issue. |
chromium.use is not a function |
The script imported standard Playwright instead of the Chromium instance from playwright-extra, or package versions do not match the documented integration. |
Import chromium from playwright-extra, then check the current package README and dependency versions. |
| The script launches but target content is missing | Navigation may have timed out, the page may require more loading time, or the target may deny or challenge automation. | Inspect navigation errors and page content in an authorized environment. Adjust waits to match the application’s loading behavior; do not assume stealth will resolve a challenge. |
| Behavior differs between local and CI | Different browser binaries, OS libraries, environment variables, browser modes, or dependency versions can change results. | Pin and log framework and plugin versions, install the intended browser in CI, and reproduce the same mode and target conditions. |
| A bot check or CAPTCHA still appears | The plugin does not prevent all detection, and the site may require a permitted API or an explicit testing arrangement. | For a site you own, use a test configuration or coordinate with its operators. For other sites, respect their access controls instead of trying to defeat them. |
7. Performance, reliability, and cost
- Performance: Browser startup and page loading usually dominate a small script. Puppeteer documents the headless shell as potentially more performant for automation tasks that do not need the full Chrome feature set. Compare modes using your own authorized workload; no universal performance number follows from that guidance.
- Reliability: Pin compatible package versions, install the corresponding browser binary, set navigation waits for the application’s behavior, and close the browser in a
finallyblock. Revalidate after browser or plugin upgrades. - Cost: The libraries are used in your own browser runtime, so operational cost depends on the compute and browser infrastructure you run. This research does not establish a benchmark or a fixed cost. For larger test fleets, account for parallel browser memory and CPU use.
- Limits: No headless mode or plugin guarantees invisibility, acceptance, or a particular result on every site. Keep testing within explicit authorization.
8. Or skip the browser setup
If the task is simply to capture a page you are authorized to access, ScreenshotNeo is a website screenshot API and MCP server for developers. It makes a screenshot or PDF from one GET request. Its capture flow accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for request options and configuration.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', bytes);
ScreenshotNeo includes full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF settings, custom CSS and JavaScript, click-before-capture, selector hiding and waits, request and resource blocking, custom headers and cookies, user agent, authorization, timezone and geolocation, transparent backgrounds, image resizing, configurable caching, signed public image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI spec. Parameter names used by other screenshot APIs also work. Pricing is free for 1,000 shots a month with no card; paid plans start at $5 for 3,000, and yearly billing gives two months free. Every feature is on every plan.
Sign up for 1,000 free screenshots a month with no card.
9. FAQ
Does stealth mode make a headless browser undetectable?
No. The plugin maintainers explicitly acknowledge that headless Chromium can still be detected, and behavior varies by browser, version, and site.
Can I use this plugin with Firefox or WebKit?
The plugin describes itself as optimized for Chromium-based browsers. Do not assume the same compatibility or behavior for Firefox or WebKit; check current package documentation for the framework and engine you use.
Should I use headful Chrome instead?
Use the mode that matches the browser environment your authorized test is intended to cover. Headful mode can help reproduce a headed workflow, but it is not a guarantee that automation will be accepted.
Is a successful public test-page result proof the setup works everywhere?
No. A test page only demonstrates the behavior it checks at that time. It cannot establish compatibility with every site or future browser version.


