Playwright Proxy Not Working: Causes, Fixes & Setup Guide (2026)
Playwright proxy issues fall into a handful of root causes — wrong configuration placement, missing authentication credentials, credential format errors, bypass rule syntax, or environment-specific network restrictions in Docker and CI. This guide covers every documented cause and fix, the correct proxy setup for both JavaScript and Python, how to verify the proxy is actually routing traffic, and how to integrate residential proxies for scraping at scale.
⚡ Key Takeaways
- Playwright proxy must include the protocol prefix —
http://proxy:portnot justproxy:port. - Proxy set at
browser.launch()applies globally. Proxy set atbrowser.newContext()applies per browser context — and overrides the launch-level proxy. - For authenticated proxies, use the
usernameandpasswordfields — not credentials embedded in the server URL string. - Playwright adds
<-loopback>to the bypass list by default —localhostand127.0.0.1always bypass the proxy unless you disable this behaviour.[1] - The fastest verification: navigate to
https://ipinfo.io/ipimmediately after launch — if the IP matches your proxy, it is working. - In Docker and CI, the proxy address must be reachable from inside the container —
localhostin the proxy URL refers to the container, not the host machine.
Correct Playwright Proxy Configuration
Before debugging, confirm your proxy configuration matches Playwright's expected format exactly. The most common cause of "proxy not working" is a syntax error in the configuration itself — missing protocol, wrong field name, or credentials in the wrong place.[2]
JavaScript / TypeScript — Global (launch level)
const { chromium } = require('playwright'); const browser = await chromium.launch({ proxy: { server: 'http://gate.nstproxy.io:8080', // ✅ include protocol prefix username: 'your_username', // ✅ separate field, not in URL password: 'your_password', // ✅ separate field, not in URL } }); const page = await browser.newPage(); await page.goto('https://ipinfo.io/ip'); console.log(await page.textContent('body')); // Should show proxy IP await browser.close();
JavaScript — Per-Context (overrides launch proxy)
const browser = await chromium.launch(); // no proxy at launch const context = await browser.newContext({ proxy: { server: 'http://gate.nstproxy.io:8080', username: 'your_username', password: 'your_password', } }); const page = await context.newPage(); await page.goto('https://ipinfo.io/ip');
playwright.config.ts — Global for all tests
// playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { proxy: { server: 'http://gate.nstproxy.io:8080', username: 'your_username', password: 'your_password', }, }, });
socks5://gate.nstproxy.io:1080. HTTP proxies use http://. Mixing these up is a silent failure — Playwright will not throw an error but the proxy will not route traffic correctly.
Step 1: Verify the Proxy Is Actually Routing Traffic
Never assume the proxy is working based on the absence of errors. Always make an explicit IP check immediately after browser launch — before running any scraping logic:
// Verification pattern — add to every Playwright proxy setup const page = await context.newPage(); // Method 1: text content of IP check service await page.goto('https://ipinfo.io/ip'); const ip = (await page.textContent('body')).trim(); console.log('Exit IP:', ip); // Must match proxy IP, not your real IP // Method 2: JSON response with full geo context await page.goto('https://ipinfo.io/json'); const data = JSON.parse(await page.textContent('body')); console.log('IP:', data.ip, '| City:', data.city, '| Country:', data.country); // Method 3: evaluate() for cleaner extraction await page.goto('https://api.ipify.org?format=json'); const result = await page.evaluate(() => JSON.parse(document.body.textContent)); console.log('Proxy IP confirmed:', result.ip);
If the returned IP matches your real machine IP instead of the proxy, the proxy configuration is not being applied — the proxy is silently bypassed, not failing with an error.
Common Errors and Exact Fixes
Playwright cannot connect to the proxy server
🔍 Cause: Wrong proxy host/port, proxy server is offline, firewall blocking the port, or the proxy address is unreachable from the current network.
✅ Fix: Test the proxy endpoint manually with curl -x http://user:pass@host:port https://ipinfo.io/ip before running Playwright. If curl fails, the issue is network/proxy server availability — not Playwright configuration. In Docker, ensure the proxy host is reachable from inside the container.
Proxy rejected credentials
🔍 Cause: Credentials are missing, incorrect, or provided in the wrong format (embedded in the server URL instead of separate fields). Playwright requires credentials in the username and password fields — not as http://user:pass@host:port in the server string.
✅ Fix: Move credentials out of the URL and into the dedicated fields. If your proxy uses IP whitelisting instead of username/password, ensure your current machine's IP is whitelisted in the proxy provider dashboard.
No error but the proxy is not routing traffic
🔍 Cause 1: Missing protocol prefix. proxy: { server: 'host:port' } is invalid — must be 'http://host:port'.
Cause 2: Context-level proxy overrides launch-level proxy with an empty/different config.
Cause 3: The target URL is on the bypass list. Playwright adds <-loopback> by default — localhost always bypasses.
Cause 4: Environment variable HTTP_PROXY / HTTPS_PROXY is conflicting with the Playwright proxy config.
✅ Fix: Add the http:// prefix. Check for context-level proxy config that might be clearing the launch config. Review the bypass field. Unset conflicting environment variables or explicitly set them to match your proxy.
Page load times out through the proxy
🔍 Cause: The proxy is reachable but the target website is slow to respond, or the proxy itself is overloaded. Default Playwright timeout is 30 seconds.
✅ Fix: Increase the timeout: page.goto(url, { timeout: 60000 }). Switch to a lower-latency proxy endpoint. Use residential proxies with health monitoring to avoid degraded IPs that add unnecessary latency.
SSL certificate error through proxy
🔍 Cause: The proxy performs SSL interception with a self-signed certificate, or the proxy's certificate chain is incomplete. Common with corporate proxies.
✅ Fix: For testing environments only: chromium.launch({ ignoreHTTPSErrors: true }) or browser.newContext({ ignoreHTTPSErrors: true }). For production, install the proxy's root certificate in the system trust store.
launch() vs newContext(): Proxy Scope Explained
This is the most common source of confusion. Playwright supports proxy configuration at two levels, and they interact in a non-obvious way:
| Level | Method | Applies To | Overridden By |
|---|---|---|---|
| Browser-level | browser.launch({ proxy: {...} }) |
All contexts and pages unless overridden | Context-level proxy config |
| Context-level | browser.newContext({ proxy: {...} }) |
Only pages created in that context | Nothing — this is the final word for that context |
browser.launch() and then call browser.newContext({ proxy: { server: 'per-context' } }) without proper credentials, the context-level config overrides the launch-level config entirely — and the proxy may stop working for that context. Always ensure context-level proxy configs are complete.
// ✅ Correct: per-context proxies for multi-account scraping const browser = await chromium.launch(); const context1 = await browser.newContext({ proxy: { server: 'http://gate.nstproxy.io:8080', username: 'user1', password: 'pass1' } }); const context2 = await browser.newContext({ proxy: { server: 'http://gate.nstproxy.io:8081', username: 'user2', password: 'pass2' } }); // Each context uses a different proxy — they never share sessions const [page1, page2] = await Promise.all([ context1.newPage(), context2.newPage() ]);
Authentication Issues: Credentials Format
Playwright's proxy authentication requires credentials in the username and password fields of the proxy object. Embedding credentials in the server URL string (http://user:pass@host:port) does not work reliably across all Playwright versions and browsers.[3]
// ❌ WRONG — credentials in URL (unreliable) proxy: { server: 'http://username:password@gate.nstproxy.io:8080' } // ✅ CORRECT — credentials in separate fields proxy: { server: 'http://gate.nstproxy.io:8080', username: 'username', password: 'password', } // ✅ ALSO CORRECT — IP whitelisting (no credentials needed) // Register your server IP in the Nstproxy dashboard whitelist first proxy: { server: 'http://gate.nstproxy.io:8080' }
Proxy Bypass Rules
Playwright's bypass field accepts a comma-separated list of hosts that should skip the proxy. There are two important behaviours to understand:[1]
- Loopback bypass by default: Playwright adds
<-loopback>to Chromium's--proxy-bypass-listautomatically. This meanslocalhostand127.0.0.1always bypass the proxy unless you explicitly disable this with the environment variablePLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK=1. - Bypass syntax: Use dot-prefixed domains (
.example.com) for subdomain matching. IP ranges require CIDR notation. Spaces after commas in the bypass string cause parsing issues in some versions.
// Bypass syntax examples proxy: { server: 'http://gate.nstproxy.io:8080', bypass: '*.internal.company.com,.local,192.168.0.0/24', // ⚠️ No spaces after commas — some versions parse incorrectly with spaces } // Allow localhost through proxy (disable automatic loopback bypass) // Set environment variable before launching: // PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK=1
Docker and CI Environment Issues
Playwright proxy issues in Docker and CI pipelines have different root causes than local development failures. The most common patterns:
Proxy Unreachable Inside Docker Container
If your proxy is running on the host machine and you reference it as localhost in the Playwright config, it will fail inside a Docker container — localhost inside the container refers to the container itself, not the host. Use host.docker.internal (Docker Desktop on Mac/Windows) or the host's bridge IP (172.17.0.1 on Linux) instead.
Corporate Proxy in CI (GitLab, GitHub Actions)
CI environments often have environment variables HTTP_PROXY, HTTPS_PROXY, and NO_PROXY set at the system level. These can conflict with or override Playwright's proxy configuration. Check with printenv | grep -i proxy in your CI pipeline. Either align Playwright's proxy config with the system proxy or unset the conflicting environment variables for the Playwright step.
Firewall Blocking Proxy Port in CI Runner
CI runner machines often have restrictive outbound firewall rules. Ports like 8080, 1080, and 3128 may be blocked. Test reachability from the CI environment itself: add a pre-step that runs curl -x http://proxy:port https://ipinfo.io/ip to confirm the proxy is accessible before Playwright tests run.
# docker-compose.yml — correct proxy reference from container # services: # playwright: # environment: # - PROXY_HOST=host.docker.internal # Mac/Windows # - PROXY_HOST=172.17.0.1 # Linux bridge // playwright.config.ts — read proxy from environment export default defineConfig({ use: { proxy: { server: `http://${process.env.PROXY_HOST || 'gate.nstproxy.io'}:8080`, username: process.env.PROXY_USER, password: process.env.PROXY_PASS, }, }, });
IP Rotation in Playwright
A single proxy IP will eventually get flagged by heavily protected targets. The correct rotation pattern in Playwright is one proxy per browser context — each context maintains its own session, cookies, and IP identity:
const proxies = [ { server: 'http://gate.nstproxy.io:8080', username: 'user1', password: 'pass1' }, { server: 'http://gate.nstproxy.io:8081', username: 'user2', password: 'pass2' }, // Add more — or use a rotating gateway that changes IP per connection ]; let proxyIndex = 0; async function scrapeUrl(browser, url) { const proxy = proxies[proxyIndex % proxies.length]; proxyIndex++; const context = await browser.newContext({ proxy }); const page = await context.newPage(); try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); return await page.content(); } finally { await context.close(); // Close context after each URL — releases the IP } } // Run 10 pages concurrently, each with a different proxy const browser = await chromium.launch(); const results = await Promise.all(urls.map(url => scrapeUrl(browser, url))); await browser.close();
server URL for all contexts and the gateway handles rotation — no need to manage a proxy list manually.
Using Residential Proxies with Playwright
Datacenter IPs are the most common cause of Playwright scraping being blocked — even when the proxy itself is configured correctly. If the proxy is working (IP check passes) but target sites still block or CAPTCHA you, the issue is IP reputation, not configuration.
Nstproxy's residential proxies provide ISP-assigned IPs that pass the IP reputation checks anti-bot systems use as their first filter. Integration with Playwright is identical to any HTTP proxy:
// Playwright + Nstproxy residential proxy — full production setup import { chromium } from 'playwright'; const browser = await chromium.launch({ headless: 'new', proxy: { server: 'http://gate.nstproxy.io:8080', username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD', } }); const context = await browser.newContext({ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36', viewport: { width: 1366, height: 768 }, locale: 'en-US', }); const page = await context.newPage(); // Confirm residential IP is active await page.goto('https://ipinfo.io/json'); const info = JSON.parse(await page.textContent('body')); console.log(`Routing through: ${info.ip} (${info.city}, ${info.country})`); // Now scrape the actual target await page.goto('https://target-site.com', { waitUntil: 'networkidle' }); await browser.close();
Python: Playwright Proxy Setup and Troubleshooting
# Python — correct proxy setup with verification from playwright.sync_api import sync_playwright import json def scrape_with_proxy(url: str, proxy_config: dict) -> str: with sync_playwright() as p: browser = p.chromium.launch( headless=True, proxy=proxy_config ) context = browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" ) page = context.new_page() # Step 1: verify proxy is routing page.goto("https://ipinfo.io/json") info = json.loads(page.text_content("body")) print(f"Exit IP: {info['ip']} ({info.get('city')}, {info.get('country')})") # Step 2: scrape actual target page.goto(url, wait_until="domcontentloaded", timeout=30000) html = page.content() browser.close() return html proxy_config = { "server": "http://gate.nstproxy.io:8080", "username": "YOUR_USERNAME", "password": "YOUR_PASSWORD", } html = scrape_with_proxy("https://example.com", proxy_config)
Debugging Checklist: 10-Step Fix Process
- Verify proxy reachability — run
curl -x http://user:pass@host:port https://ipinfo.io/ipbefore Playwright. If curl fails, the issue is network/proxy server, not Playwright. - Check protocol prefix — ensure
serverstarts withhttp://orsocks5://. Missing prefix = silent failure. - Move credentials to fields — use
usernameandpasswordfields, not embedded in the server URL string. - Run IP verification — navigate to
https://ipinfo.io/ipimmediately after launch to confirm the proxy IP is active. - Disable headless temporarily — run
headless: falseto visually inspect what the browser is doing during navigation. - Check for context-level proxy override — if you set proxy at launch AND at context level, context wins. Ensure context config is complete.
- Review bypass list — confirm the target URL is not in the bypass list and the loopback bypass is not interfering.
- Test in the actual runtime — in Docker or CI, run the curl test from inside the container/runner, not your local machine.
- Check environment variable conflicts — run
printenv | grep -i proxyto find system proxy variables that may override your config. - Increase timeout — if the proxy works on curl but times out in Playwright, add
timeout: 60000topage.goto().
Eliminate IP-Based Blocks with Nstproxy Residential Proxies
Playwright proxy configured correctly but still getting blocked? The issue is IP reputation — not configuration. Nstproxy's 110M+ residential IPs pass anti-bot checks that reject datacenter IPs on sight.
Try Nstproxy for Free →FAQ
The most common cause is missing protocol prefix (http://) in the server string, credentials embedded in the URL instead of separate fields, or a context-level proxy config overriding the launch-level config. Run the IP verification check (page.goto('https://ipinfo.io/ip')) immediately after launch to confirm whether the proxy is routing traffic at all, then use the debugging checklist above to narrow the cause.
Pass the proxy config to browser.newContext({ proxy: {...} }) instead of or in addition to browser.launch({ proxy: {...} }). The context-level proxy overrides the launch-level proxy for all pages in that context. This is the correct pattern for running multiple browser sessions with different proxy IPs simultaneously — one context per proxy.
Playwright automatically adds <-loopback> to Chromium's proxy bypass list, meaning localhost and 127.0.0.1 always bypass the proxy by default. To disable this behaviour, set the environment variable PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK=1 before running Playwright.
Create a new browser context for each URL or session, passing a different proxy config to each context. Each context maintains its own cookies, storage, and IP identity. Close the context after each session to release the IP. For simpler rotation, use a provider like Nstproxy with a rotating residential gateway — each new TCP connection from the same credentials gets a different IP automatically, without managing a proxy list in your code.
Check three things: (1) the proxy address — localhost inside a container refers to the container, not the host machine; use host.docker.internal or the bridge IP 172.17.0.1 instead; (2) run printenv | grep -i proxy in the CI pipeline to find conflicting system proxy variables; (3) test proxy reachability from inside the container with curl -x http://proxy:port https://ipinfo.io/ip as a pre-step to confirm network accessibility.

