Playwright Stealth: Configure Anti-Detection for Web Scraping in Python and Node.js
Headless browsers in Playwright can leak fingerprint signals that anti-bot systems notice. While Playwright is great for automation, its default settings make these signals easy to spot. Stealth plugins help cover these leaks so your scripts look like real user traffic. This guide explains detection methods, how to set up stealth in Python and Node.js, what gets patched, how to test, and the limits and scaling options.
Kipras Kalzanauskas
Last updated: Jul 13, 2026
11 min read

TL;DR
- Playwright stealth isn't a built-in feature. It's third-party packages, playwright-stealth for Python and playwright-extra paired with the Puppeteer stealth plugin for Node.js, that patch fingerprint leaks before page scripts execute.
- Stealth only fixes browser-level signals. It won't touch IP reputation, TLS fingerprinting, or behavioral analysis, so pair it with proxies and randomized timing for serious targets.
- Both ecosystems only support Chromium. Firefox and WebKit have no stealth implementation in either language.
- Test before you scrape. Check navigator.webdriver, plugin count, and User-Agent against a fingerprint test page before pointing the scraper at production.
How websites detect Playwright and headless browsers
Before using stealth patches, it helps to understand what they fix. Anti-bot systems use several detection layers, so a script that passes one check might still get caught by another.
Browser fingerprinting
Websites check things like navigator properties, installed plugins, WebGL renderer strings, supported media codecs, screen size, and many other details to profile visitors. Each value alone isn't suspicious, but headless Chromium often reports settings that real users rarely have, like an empty plugins array or a generic WebGL renderer string. Together, these details create a fingerprint that can flag your session as automated before it even reaches a protected page. The guide on bypassing CreepJS and spoofing browser fingerprinting explains how this scoring works.
Automation markers
Some signals exist only because the browser is being driven by code. navigator.webdriver defaults to true in any Chromium instance launched through CDP. Older headless builds inject the substring HeadlessChrome into the User-Agent string. And Chrome-only APIs like chrome.app and chrome.runtime are missing entirely, because they were never exposed to the automated context in the first place. Any one of these is enough for a basic bot check to reject the session outright.
Behavioral analysis
Detection doesn't stop at page load. Anti-bot systems track mouse movement curves, scroll velocity, time between clicks, and the order in which pages get visited. A script that jumps straight from page load to button click, with no mouse movement in between, doesn't look human, even when every fingerprint value checks out.
Consistency checks
Detection systems also cross-reference signals against each other. A User-Agent that claims to be Windows paired with a navigator.platform value that reports Linux is an immediate red flag, and so is a screen resolution that doesn't match the claimed device type. Spoofing one value without touching the others creates exactly the mismatch these checks are built to catch.
Network-level detection
None of this matters if the connection is already flagged. IP reputation scoring blocks known datacenter ranges and proxy subnets before the browser sends a single request. TLS handshakes leak their own signature too, a technique called TLS fingerprinting. The specific standard has shifted: JA3 was the workhorse for years until Chrome 110 randomized TLS extension order in 2023 and broke it, so most enterprise vendors now run on JA4 instead. Either way, this happens at the network layer, completely separate from anything a stealth patch can touch inside the page.
Stealth plugins target the first two layers: fingerprinting and automation markers. The rest needs proxies, behavioral mimicry, or both, which later sections in this guide cover.
What is Playwright stealth and how does it work?
Nothing inside Playwright itself has a stealth setting. The name refers to third-party packages bolted onto the framework that patch browser fingerprints before any page script gets a chance to run, closing the gaps from the previous section.
Python and Node.js solve this differently. Python gets a dedicated package, playwright-stealth, currently at v2.0.3. It started as a port of the Puppeteer stealth plugin and has since grown its own bundled evasion scripts plus a few Python-only additions. Node.js doesn't have a Playwright-native equivalent at all. Instead, you pair playwright-extra with puppeteer-extra-plugin-stealth: playwright-extra is a thin wrapper that lets the original Puppeteer stealth plugin run against Playwright's API, so what you're actually running is Puppeteer's evasion code, not something built for Playwright.
That distinction matters more than it sounds. The underlying patches, navigator overrides, Chrome API shims, and WebGL spoofing are nearly identical between the two because they trace back to the same source. What's different is ownership: Python forked and maintains its own copy, while Node.js depends on a wrapper around a project that hasn't shipped anything new in years.
Both are Chromium-only. Neither package touches Firefox or WebKit, so cross-browser stealth coverage isn't on the table no matter which language you pick.
That maintenance gap is the real story here. playwright-stealth has shipped four releases in the past year, the latest in April 2026. playwright-extra and puppeteer-extra-plugin-stealth haven't been updated since March 2023. The Node.js stack still passes basic checks, but it isn't absorbing new evasions as detection techniques move forward. If you're choosing a language for a new project and don't already have a Node.js codebase to maintain, that's a real point in Python's favor.
For more on how the two frameworks compare outside of stealth, see Puppeteer vs. Playwright. If you haven't set up a basic Playwright scraper yet, Playwright web scraping covers what this guide assumes you already know.
Feature
Python (playwright-stealth)
Node.js (playwright-extra + plugin)
Active maintenance
Active, v2.0.3 released April 2026
Stale, no core updates since March 2023
Engine architecture
Native Python fork/port
Thin wrapper around Puppeteer's plugin
Configuration style
Constructor keyword arguments
JavaScript Set of string identifiers
Browser support
Chromium-only
Chromium-only
What stealth plugins actually patch in the browser
Each evasion module patches a specific sign. These aren't unusual; they're targeted fixes for the automation markers and fingerprint mismatches that a headless browser shows by default.
Chrome API emulation
Headless Chromium is missing several APIs that only exist in a real Chrome window: chrome.app, chrome.csi, and chrome.loadTimes. Stealth fakes their presence, so a fingerprinting script that checks for them gets the expected object instead of undefined. chrome.runtime gets the same treatment, mocking the extension API surface that real Chrome exposes, though it ships disabled by default in the Python package's v2.x line because it can break sites that probe it too aggressively. Enable it explicitly if your target needs it.
Navigator property patches
navigator.webdriver is the most frequently checked property. Plain Chromium sets it to true, but stealth changes it to undefined to match a real session. navigator.plugins is filled with realistic entries instead of an empty array. navigator.languages is set to values that match a real locale, and navigator.permissions is patched so permission queries don't return the inconsistent results that headless Chrome is known for. navigator.vendor and navigator.hardwareConcurrency are also adjusted to stay consistent with a real desktop Chrome installation.
Rendering and media patches
WebGL reveals its own fingerprint through the vendor and renderer strings it reports, and headless Chromium's defaults don't match real hardware. Stealth overrides both. The media codec list is also faked, since headless builds report a smaller set of supported codecs than regular Chrome, which can be a giveaway.
Automation marker removal
Some patches are designed only to remove signs that the browser was launched by code instead of a person. iframe.contentWindow is fixed so cross-origin iframe access doesn't throw the specific error caused by automation. defaultArgs removes the --enable-automation flag and related launch flags from Chromium's command line before it starts. sourceurl removes a marker that injected evasion scripts can leave in stack traces, and error_prototype goes further by overriding Error.name, since a raw stack trace pointing to an injected script is a clear sign of automation. user-agent-override keeps the User-Agent string consistent across all places it appears, such as the HTTP header, navigator.userAgent, and Sec-CH-UA Client Hints, because mismatches between these are exactly what anti-bot systems check for.
Module count
Both packages include about fifteen to twenty individual evasion modules, depending on whether you count smaller ones like accept-language and console.debug. The Python package ports almost all of them from the original Puppeteer stealth plugin and adds a few of its own, such as error_prototype, which is unique to the Python fork. The main difference beyond that is configuration: Node.js uses a Set of evasion names, while Python uses keyword arguments in the Stealth() constructor.
Setting up Playwright stealth in Python
Create a dedicated project directory before writing any code:
Set up a virtual environment so this project's dependencies stay isolated from everything else on your machine:
Check your Python version before going further. playwright-stealth requires 3.9 or higher:
Create the main script file:
Your project should now look like this:
All code in this section goes inside scraper.py unless stated otherwise.
Installing playwright-stealth
playwright-stealth installs Playwright as a dependency, but the browser binary must be downloaded separately. Since stealth only works with Chromium, that's the only browser you need to install. You can skip Firefox and WebKit.
The async pattern (recommended)
For scraping workloads, wrap async_playwright() with the Stealth().use_async() context manager. Every page created inside it, across every context, gets every evasion applied automatically:
The use of page.goto or browser.close doesn't change. The context manager handles browser and context creation behind the scenes and injects the evasion scripts before anything else runs. It also patches some Chromium launch flags directly, which JavaScript-only patches cannot do.
The sync pattern (quick scripts and prototyping)
For a one-off script or quick debugging, Stealth().use_sync() wraps sync_playwright() the same way:
Use this approach when testing something interactively. If you need to run more than a few pages, the async pattern manages concurrency more effectively.
Manual application with apply_stealth_async
The context manager wraps the whole Playwright instance, which doesn't always fit. If part of your workload needs stealth and part doesn't, or your code already manages the browser lifecycle elsewhere, apply it to a specific context instead:
Only pages created from that context after apply_stealth_async runs will have the evasions. Make sure to call it before new_page(), not after.
Customizing which evasions run
Stealth() takes its configuration as keyword arguments directly, there's no separate config object. Override a specific value, or start from nothing and enable evasions one at a time:
ALL_EVASIONS_DISABLED_KWARGS helps you isolate exactly which evasion a target checks for, so you don't have to guess.
Don't follow tutorials using stealth_async(page)
Many older tutorials still show stealth_async(page) or stealth_sync(page) called directly on a page object. That was the original v1.x API, which no longer exists in the current package. The 2.0.0 rewrite in mid-2025 replaced it with the Stealth class, switched to keyword-only configuration, and added the ability to patch Chromium's launch arguments along with the JavaScript evasions. If a guide imports stealth_async directly from playwright_stealth, it's describing a version that has been gone for about a year.
For targets that render content after the initial load, how to scrape websites with dynamic content covers waiting strategies. And for locating elements once the page renders, Playwright XPath covers selector syntax.
Full code
Setting up Playwright stealth in Node.js
Create a dedicated project directory before writing any code:
Check your Node version before going further. Playwright requires Node 18 or higher:
Create the main script file:
Your project should now look like this:
Installing the packages
You need three packages: playwright itself, the playwright-extra wrapper, and the stealth plugin. All three are required. If you skip one, the setup will not give a clear error, it just will not work.
The import that actually matters
This is the one mistake that breaks the entire setup with no warning: import chromium from playwright-extra, not from playwright.
playwright-extra wraps the regular chromium launcher with a plugin system but keeps the rest of the API the same. If you import from the plain playwright package, chromium.use() either doesn't exist where you expect it or your editor autocompletes the wrong import, and the script runs without any stealth applied.
Registering the stealth plugin
Call chromium.use(StealthPlugin()) before launching any browser instance, it has to run before chromium.launch(), not after.
CommonJS:
ESM:
For the ESM version, add "type": "module" to package.json or save the file with an .mjs extension instead.
Customizing which evasions run
By default, StealthPlugin() enables every evasion module it ships with. Pass an enabledEvasions Set to run a specific subset instead:
Anything not included in the Set remains unpatched. Check StealthPlugin().availableEvasions for the full list of module names in your version, as this can change between releases.
TypeScript
playwright-extra and most of its plugins are written in TypeScript and ship their own type declarations, so chromium.use(), StealthPlugin(), and everything else get full autocomplete and type checking with no separate @types package to install.
For broader Node.js scraping patterns outside of Playwright, see web scraping with Cheerio and Node.js. For more on Node.js itself, the glossary entry covers the runtime basics this guide assumes.
Full code
Testing and verifying your stealth setup
One of the biggest mistakes is using stealth and just assuming it works. Usually, you only notice something is wrong when you start getting blocked in production without any clear error messages. By that point, you've already wasted the proxy budget and lost data on a run that seemed fine.
Browser fingerprint test pages
bot.sannysoft.com is a common place to start testing. It checks many automation signals, such as navigator.webdriver, plugin count, WebGL renderer, and User-Agent consistency, all on one page. Each signal is marked green or red. While it's not a full test, the CreepJS-based check in the section about bypassing CreepJS and spoofing browser fingerprints is more thorough; it's the fastest way to spot a broken stealth setup before using it on a real target.
For a quick manual check during development, navigate to the test page and save a screenshot:
Open the screenshot and check for any red rows. This method is good for a quick check, but it doesn't scale. It also won't catch issues that appear later, such as when a dependency update quietly breaks one of your evasions weeks later.
Programmatic verification
If you plan to run your setup more than once, use page.evaluate() to get the actual fingerprint values and check them directly, instead of just looking at a screenshot:
You can use the same approach in Node.js. The page.evaluate() function works the same way there. Just use the assertions that match your test runner.
Verification checklist
- navigator.webdriver should return undefined, not true. Some older guides say it should be false, but undefined is what a real, unautomated Chrome session shows, and that's what the patches in this guide aim for
- navigator.plugins.length is greater than 0
- navigator.languages is a populated array, not empty
- navigator.vendor returns “Google Inc.”
- The User-Agent string contains no HeadlessChrome substring
Ongoing monitoring
A stealth setup that works today might stop working next month, even if you haven't changed your code. Evasion modules can become outdated, target sites add new checks, and sometimes a dependency update breaks a patch. Run the verification function above on bot.sannysoft.com and a few target pages regularly, especially for anything in production. If you see a failure, check for package updates before assuming the target site changed. For more on what to do when stealth isn't enough, see the section on web scraping without getting blocked.
Limitations of Playwright stealth
Stealth only patches one layer: the JavaScript properties and browser environment signals that fingerprinting scripts can read. This is important, and many anti-bot systems stop at this layer, but it's not the only thing that matters. Stealth doesn't help with anything outside this layer.
IP reputation
This is checked before your stealth patches even come into play. Datacenter IP ranges and known proxy subnets are often scored and blocked at the network level, sometimes before the browser even sends a request. Even if your fingerprint is perfect, if your IP is flagged, you'll still get blocked. In these cases, the fingerprint isn't the issue.
TLS fingerprinting
Every TLS handshake produces its own signature. The original JA3 fingerprint standard lost most of its reliability after Chrome 110 started randomizing TLS extension order in 2023, so by 2026 most enterprise anti-bot vendors, Cloudflare and DataDome included, score connections against JA4 instead, which sorts before hashing specifically to survive that randomization. Either fingerprint gets evaluated at the network layer, before a single JavaScript evasion gets a chance to run. No amount of property spoofing touches this.
Advanced JavaScript challenges
Cloudflare, DataDome, and PerimeterX go beyond just reading navigator properties. They use cryptographic proof-of-work challenges, server-side session checks, and detection methods that change faster than open-source stealth plugins can keep up. Even a well-maintained plugin might only handle last year's checks, while these vendors release new ones every week.
Behavioral analysis
None of this covers how a session behaves over time. Things like mouse movement patterns, scroll speed, time between clicks, and the order of page visits aren't affected by stealth. If your script loads a page and clicks a button without any mouse movement, it still looks like a bot, no matter how good your fingerprint is.
When stealth alone is enough, and when it isn't
Stealth is enough for sites running basic protections: a navigator.webdriver check, a missing-plugins check, a simple User-Agent filter. That covers a large share of the web. Most sites aren't running enterprise anti-bot infrastructure.
Stealth stops being enough when a target uses a well-known anti-bot vendor, needs to handle large-scale scraping, or shows signs like unexpected CAPTCHAs, fast IP bans, or files returning as HTML instead of data. At that point, you need to decide whether to add proxy rotation and behavioral mimicry, or use a managed service. For sites protected by Cloudflare, stealth alone usually only works on the lowest security settings.
Advanced stealth strategies for scraping at scale
Stealth helps you get started, but the rest of this section is about staying in long enough to collect the data you need, without wasting proxies and sessions.
Session management
Persist cookies and storage state within a session instead of starting cold on every request. Playwright supports this directly:
Don't let a session run forever. Rotate sessions on a set schedule, after a certain number of requests, or as soon as you notice the block rate going up. Also, keep each session limited to a single target. Using the same session and cookies across different sites can create its own fingerprint issues
Mimicking human behavior
Stealth only fixes the fingerprint. It doesn't help with timing, and perfectly consistent timing is a giveaway. Add some randomness to things like scroll depth, how long you stay on a page before acting, and the time between actions:
You don't need perfect realism, since that's not practical. The main goal is to avoid the obvious sign of a script that always goes from page load to click in just a few milliseconds.
CAPTCHA handling
Try to detect a CAPTCHA as soon as possible, instead of finding out after a timeout. Signs include an empty DOM where content should be, an interstitial page, or a redirect loop back to the same URL. These all mean you've hit a challenge instead of the real page.
Once you've detected a CAPTCHA, follow a recovery ladder instead of blindly retrying the same request. Retry once, then start a new session, then switch IPs. If none of that works, do a full reset of the session, IP, and fingerprint together. Treat a rising CAPTCHA rate as a sign to back off and reassess, not as an obstacle to push through with more retries.
How to bypass CAPTCHAs: the ultimate guide covers the recovery ladder in more depth, and how to bypass Google CAPTCHA is worth a look if reCAPTCHA specifically is what you're running into.
Proxy integration
Stealth and proxies address different issues, and you need both for serious scraping. Stealth fixes the fingerprint, while proxies handle IP reputation. The usual setup for production scraping is to combine stealth with rotating residential proxies. Make sure the proxy's location matches your fingerprint's locale settings. For example, if your session claims en-US languages and a New York timezone but uses a Frankfurt IP, that's a mismatch and can be detected.
Concurrency and rate limiting
Throttle your requests by domain, not across all sites. One site might allow 10 requests per second from one IP, but block the same rate from another IP pool. There's no single safe rate for all cases. Start with a small test batch before increasing volume, and slow down as soon as you see more blocks, instead of waiting for a full outage. Adaptive concurrency, which means reducing parallelism when failures go up, helps catch issues faster than a fixed worker pool. For more on retries, see the section on retrying failed Python requests.
Monitoring and maintenance
Track your success rate, block rate, and CAPTCHA rate for each domain, not just overall. A 95% success rate across all sites can hide a domain that's failing completely. Save screenshots and HTML snapshots when something fails. These help you quickly spot whether it's a selector issue or a detection problem, especially if it happens late at night. Also, set up a regression suite that tests your stealth setup on a few key targets regularly. This way, you'll catch broken evasions or outdated selectors before they cause silent drops in your data.
For background on why proxy rotation matters here in the first place, see what are rotating proxies?
Legal and ethical considerations for stealth scraping
Stealth makes your scraper harder to detect, but it doesn't make it legal or ethical. These are separate issues. Ignoring them just because you solved the technical challenges can lead to being blocked or even facing legal trouble.
robots.txt and terms of service
Always check robots.txt before scraping and follow its rules. Stealth can help you get past technical defenses, but it doesn't change the fact that many sites' terms of service ban automated access. Just because you're undetectable doesn't mean you're not breaking the agreement or what a court might see as reasonable use.
Rate limiting
Throttle your requests by domain and endpoint, no matter how much your infrastructure can handle. Even if your scraper can send 50 requests per second, you shouldn't do that if the target is a small site with limited resources. Scraping politely isn't just ethical; it also helps prevent the site from adding tougher defenses.
Data handling
If you collect personal data from EU citizens, GDPR applies no matter where your scraper is running. Only store personal information you actually need, and make sure you have a valid reason for keeping it. If you want to use the scraped data for something different than what you collected it for, check the site's terms and the law before doing so.
Operational guardrails
Keep a list of approved targets instead of letting your scraper run on any site it finds. Don't pretend to be a privileged crawler like Googlebot to get special access. Sites that catch this usually respond with strict enforcement. Handle credentials securely, just like in any other production system: don't hardcode or log them. Also, remove personal information from logs, screenshots, and debugging files before they end up in bug trackers or shared channels.
When to use an official API instead
If a target has an API, even if it's rate-limited or paid, it's usually a better choice than stealth scraping. APIs are more stable, less likely to break with site changes, and don't have the same legal risks as scraping around defenses. Use stealth only when there's no official way to get the data.
For more on where the legal lines generally sit, what is web scraping? covers the broader context. Decodo isn't neutral here either; the company co-founded the Ethical Web Data Collection Initiative, a set of industry guardrails for exactly the practices covered in this section.
None of this is legal advice. The specifics vary by jurisdiction and by what you're scraping, so loop in counsel before running anything at scale that touches personal data or a site's terms of service.
Final thoughts
Stealth plugins fix the first layer that most basic detection checks look for: browser fingerprints and automation markers. This is useful and lets you access a lot of sites with minimal setup. But it's not the whole solution. IP reputation, TLS fingerprinting, and behavioral analysis are outside the reach of any JavaScript patch, no matter how good the package is.
A strong setup combines stealth with rotating residential proxies, careful session management, and enough behavioral randomness to avoid obvious patterns. Each part is important. If you skip any one, you risk having a single point of failure.
If a target becomes too difficult for your layered setup, such as when anti-bot vendors are involved, CAPTCHA rates stay high, or IP bans happen faster than you can rotate addresses, it's time to consider a managed solution. Decodo's Scraping API takes care of fingerprint management, anti-bot bypass, and proxy rotation in one place, so your team can focus on the data instead of maintaining a stealth setup.
Supercharge your Playwright scripts
Handle the browser rendering with Decodo's Web Scraping API, which also manages IP rotation and CAPTCHA bypass for a completely frictionless workflow.
About the author

Kipras Kalzanauskas
Senior Account Manager
Kipras is a strategic account expert with a strong background in sales, IT support, and data-driven solutions. Born and raised in Vilnius, he studied history at Vilnius University before spending time in the Lithuanian Military. For the past 3.5 years, he has been a key player at Decodo, working with Fortune 500 companies in eCommerce and Market Intelligence.
Connect with Kipras on LinkedIn.
All information on Decodo Blog is provided on an as is basis and for informational purposes only. We make no representation and disclaim all liability with respect to your use of any information contained on Decodo Blog or any third-party websites that may belinked therein.


