Playwright-Stealth + Proxy: Pass Bot Checks

Playwright-stealth hides automation flags such as navigator.webdriver. It does not hide a datacenter IP. Sites still return 403, 429, or a Cloudflare challenge when the exit is already burned. Launch Chromium with an HTTP proxy, apply stealth on the context, and keep one sticky residential session for a single job.
Key Takeaways
- Stealth patches JS fingerprints. It does not change the exit IP.
- Datacenter and VPN exits fail first on 403, 429, or Cloudflare.
- Sticky residential session, for one scrape job.
- Rotate between jobs. Mid-job rotation drops cookies and looks like a new device.
What playwright-stealth changes in the browser
Playwright-stealth is a Python package that injects init scripts into Playwright Chromium so common automation leaks look less like a bot. It is not a new browser. It does not change your exit IP.
The same idea shows up as playwright stealth. It ports the puppeteer evasions.
The scripts run before page JavaScript. Typical patches cover navigator.webdriver, some HeadlessChrome marks in the User-Agent, and some window.chrome and plugin gaps. Confirm each flag against the package README. Do not treat any blog list as a complete evasion catalog.
The maintainers call the project a proof of concept. They say it only covers simple bot checks. TLS fingerprints, request cookies, request timing, and the public IP are outside the package. Version 2.0.3 on PyPI wraps Playwright. It does not replace Chromium.
Use the package to clean the easy JS leaks. Pair it with an HTTP proxy for the network path. Public page monitoring is in scope. Do not steal another user’s session cookie. Do not drive a login wall that is not yours.
Confirm the patch on a blank page before you hit the target. page.evaluate("navigator.webdriver") should be falsy after the wrap. If it is still true, the page was created outside the stealth session. Headless Chromium still needs the wrap. Headed mode does not replace it.
Do not inventory every evasion in your runbook. The README changes when Chromium changes. Re-check webdriver and the User-Agent after each playwright install. If those two still leak, the rest of the script does not matter yet.
Why do bot checks still fail after stealth?
A bot check is the site’s test that a visitor is a real browser on a normal network, not a script. It can read JavaScript fingerprints, the exit IP, and how fast you request pages.
Playwright-stealth only patches some fingerprints. A burned datacenter IP still returns 403, 429, or a Cloudflare challenge.
Three layers show up on scrape jobs. The first is JS in the page. The second is the ASN and reputation of the exit. The third is how many tabs you open per minute. Stealth only touches the first layer. A lab page can go green while the real host still blocks you.
Hosting ranges and shared VPN exits sit on known ASNs. Sites score that pattern before they care about navigator.webdriver. Patching flags on a dirty exit wastes a retry. Move the browser onto a residential session, then keep playwright-stealth on.
Cloudflare here is a failure shape, not a product to attack. If the challenge page loads, fix the exit and the pace. Do not install a second stealth plugin on the same burned IP.
Which proxy type should Playwright use?
Proxy type here means how the exit IP is assigned, not the brand on the box. Datacenter IPs come from hosting networks. Residential IPs come from ISP ranges.
Sticky keeps one residential IP for a session. Rotating hands you a new IP between requests or sessions.
Playwright scraping jobs use sticky residential. Account logins need a dedicated IP per account.
Route Playwright through a sticky residential session so one context keeps one exit until the job ends. IPOasis dynamic residential starts at $0.78/GB. Unused traffic does not expire. When the sticky session ends, the IP can change. Open a new sticky session for the next job.
Teams that collect public pages at scale without sharing one datacenter exit still pin one session per browser job. Match locale and timezone to the exit country. Do not point a US context at a JP IP and expect the bot check to pass.
Sticky is for one context that must keep cookies, localStorage, and the same TLS session. A product page crawl that follows pagination belongs here. Rotating residential is for independent GETs with no state. Mixing both in one context looks like a new device on every click.
Providers usually put the session flag in the proxy username, not in Playwright. Keep that username stable for the job. Change it only when you open the next context. Read host, port, user, and pass from environment variables.
How do you add a proxy to Playwright-stealth?
Adding a proxy to Playwright-stealth means sending Chromium’s traffic through an HTTP or SOCKS exit before any page loads. Playwright’s own proxy dict on launch or new_context does that.
The stealth package only injects fingerprint scripts. You need both. Username and password go in server, username, and password.
The Python network docs list those three keys. Set the proxy on the browser or on a context. Then wrap Playwright with stealth so every new page gets the init scripts. A page created without that wrap still leaks navigator.webdriver.
- Install playwright and playwright-stealth, then run playwright install chromium.
- Read host, port, user, and pass from environment variables. Do not commit secrets.
- Pass proxy={...} into chromium.launch or new_context.
- Start Playwright through Stealth().use_async(...) so new pages get the patches.
- Open the target URL on that same context.

Expected output: navigator.webdriver is falsy, and the IP body is not your home broadband. 407 or ERR_TUNNEL_CONNECTION_FAILED is a bad host, port, or password. That is not a missing stealth import. Align timezone and locale with the exit country. Do not paste a live gateway into a public gist.
SOCKS5 uses server like socks5://HOST:PORT. HTTP and HTTPS use http://HOST:PORT. Playwright documents both. Username and password still sit next to server. If the provider only supports user:pass in the URL, split them into the three fields anyway. A single URL string with embedded credentials is harder to rotate in env.
Launch-level proxy covers every context in that browser. Context-level proxy lets you pin one sticky IP per job and open a second context later with a new session. Create pages from the context that already has proxy. browser.new_page() after a context-level proxy can skip that exit. Set timezone_id and locale on the same context so the JS clock matches the IP country.
How do you know the bot check passed?
A passed bot check means the target site returned 200 and served the page you asked for. A lab fingerprint page such as sannysoft only shows whether JavaScript leaks are patched.
That is not the same test. 403, 429, or a Cloudflare challenge on the real URL means the exit IP or the request rate failed, even if stealth looks clean.
Keep one sticky IP inside the context. Do not rotate mid-job. Cookies and storage look like a new device if the address changes. After the session ends, start a new sticky session. Rotate between jobs, not inside one run.
If 403 remains after a new residential session, drop concurrency. Do not chain a captcha solver on top of playwright-stealth. That is a different failure. Check the proxy before you blame stealth. Match the browser profile to the exit IP if timezone and language still disagree with the country.
Log response.status and page.url after goto. A 200 that lands on a challenge path is still a fail. Save a screenshot of the first screen only when you are debugging. Do not dump full HTML into logs on every row.
Target sites may forbid automation in their terms. Stay on public pages or hosts you operate. Stop if the response is a login wall that is not yours.
If WebRTC still shows a second address, close extra browser contexts. One context, one sticky IP, one job.
Playwright-stealth vs puppeteer-extra-plugin-stealth
Playwright-stealth is the Python package on PyPI that wraps Playwright and injects stealth init scripts. puppeteer-extra-plugin-stealth is the original JavaScript evasion set. With Node you load it through playwright-extra.
Both hide the same class of Chromium leaks. Neither sets the proxy. The official server field does.
The JS plugin lives in the puppeteer-extra stealth package. Use it when the scraper is already Node. Stay on Python when the rest of the pipeline is Python. JS patches have a ceiling. A patched browser kernel is a different stack. Do not treat it as a drop-in for this launch config.
On Node, call chromium.use(StealthPlugin()) before launch. Then pass the same proxy object Playwright already documents. Do not load the Python package and the JS plugin in one process. Pick one stack and keep the sticky username stable for that context.
Jobs that keep one sticky session for a browser job follow the same rule on either language. Pin the exit for the whole context. Rotate after you store the page. If you later swap Chromium for a patched build, the proxy dict stays. The stealth import is what changes.
FAQ
Does playwright-stealth still work in 2026?
It still patches simple Chromium leaks. PyPI ships 2.0.3. Sites that only look at navigator.webdriver will often go quiet. Sites that score ASN, TLS, and rate will not. Treat the package as the JS layer. Put a sticky residential proxy under it. Re-test the real URL after each Chromium bump.
Can playwright-stealth pass Cloudflare by itself?
No. Cloudflare can challenge the IP and the TLS stack even when JS flags look fine. A green sannysoft screenshot does not mean the target returned 200. Move off datacenter and VPN exits. Keep one sticky residential session for the job. Slow the queue if you still see a challenge page.
Should I use a datacenter proxy with Playwright-stealth?
Use datacenter exits to hit hosts you own, or to debug launch config. Public scrape targets often already list hosting ASNs. Stealth will not hide that range. Switch to sticky residential when the first 403 appears on a datacenter IP. Do not keep adding browsers on the same blocked address.
How long can a sticky residential session last?
IPOasis Dynamic residential sticky sessions last up to 120 minutes. The IP can change when that window ends. Keep the same session for one Playwright context. Start a new sticky session for the next job. Do not apply a 120-minute cap to a static dedicated IP. That product stays on one address until you release it.
Do I apply stealth on the browser or on the context?
Wrap the Playwright driver so every new page gets the init scripts. Stealth().use_async(...) does that for Python. Applying stealth to one page and then calling browser.new_page() without the wrap leaks flags again. Set proxy on launch or on new_context. Then create pages from that context only.
Why do I still get 429 after stealth and a residential proxy?
429 is a rate limit on that exit, not a missing webdriver patch. You sent too many requests through one IP. Wait. Cut concurrency. Do not rotate mid-job if cookies must stay. Open a new sticky session only after the current job is stored. A second stealth plugin will not raise the cap.
Is puppeteer-extra-plugin-stealth the same as playwright-stealth?
They share the same evasion idea. puppeteer-extra-plugin-stealth is the JS set, loaded through playwright-extra on Node. playwright-stealth is the Python wrap on PyPI. Proxy setup is Playwright’s server, username, and password on both. Pick the package that matches your language. Do not run both in one process.
Conclusion
Stealth hides JS flags. It does not hide a burned exit. Put Playwright on a sticky residential session, and keep that IP until the job is stored. Route Playwright through a sticky residential session when you pin the context.


