Playwright Proxy Authentication Not Working: 3 Real Fixes
By Nicholas St. Germain —
Your proxy credentials pass a curl test, but the exact same proxy fails in Playwright on Chromium: the page hangs, dies with net::ERR_TUNNEL_CONNECTION_FAILED, or loops on 407 responses in your proxy logs. This happens because Chromium strips the user:pass authentication portion out of a --proxy-server URL and never sends those credentials at all.
This is documented Chromium behavior, not a Playwright bug, and probably not your provider's fault either. Once you know where the credentials are supposed to go, the fix is about four lines.
Below: what Chromium does with URL-embedded credentials, then three working setups. Credentials in the launch options, per-context proxies for multi-identity sessions, and a local forwarder for tools that only accept a bare proxy URL. All code is runnable Node.js as pasted.
Why Chromium drops proxy authentication credentials
Chromium's networking documentation is blunt about this. From net/docs/proxy.md: "Most platforms' manual proxy settings allow specifying a cleartext username/password for proxy sign in. Chrome does not implement this, and will not use any credentials embedded in the proxy settings." There is a long-standing Chromium tracker entry for exactly this behavior on the command line.
So when Chromium launches with --proxy-server=http://user:pass@1.2.3.4:8080, or you paste that whole string into Playwright's server field, the credentials are silently discarded and the browser connects to the proxy unauthenticated. What you observe next depends on the target and the proxy:
- The proxy answers
407 Proxy Authentication Required. In headed Chrome that raises a login prompt; in headless there is no prompt, so the request just fails, as covered in a Chromium-dev mailing list discussion of proxy authentication in headless Chrome. - For HTTPS pages, the rejected CONNECT surfaces as
net::ERR_TUNNEL_CONNECTION_FAILED. - Some proxies drop unauthenticated connections outright, which looks like an infinite hang.
curl behaves differently because curl parses credentials out of the URL and sends the Proxy-Authorization header itself. That mismatch is the classic signature of this whole problem: works in curl, dead in Playwright. If you want the protocol-level anatomy of the 407 exchange, we broke it down in our guide to fixing proxy 407 authentication errors.
One wrinkle before the fixes. Even when you hand Playwright credentials the right way, Chromium sends them reactively, not preemptively. A Playwright maintainer spelled this out on issue #32567: the browser "usually constructs the Proxy-Authorization header, but most likely only after the proxy returns 407 Proxy Authentication Required for the unauthorized request." In that report, an Apache Traffic Server upstream that expected a preemptive header logged "Proxy-Authorization header not found" despite a correct Playwright config. The issue was closed for lack of a reproduction, but the reactive-auth explanation in it matches Chromium's documented flow. If your proxy demands the header on the very first request, that's a proxy configuration problem, not a Playwright one.
Fix 1: credentials in the proxy object at launch
Playwright's supported path is explicit username and password fields on the proxy option, shown in the official network docs. Playwright wires the credential handling into the browser for you. Nothing goes in the server URL.
// npm i playwright
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({
proxy: {
server: 'http://proxy.example.com:8080', // scheme, host, port. No user:pass here.
username: 'your-username',
password: 'your-password',
},
});
const page = await browser.newPage();
await page.goto('https://api.ipify.org?format=json');
console.log(await page.textContent('body')); // should print the proxy's IP
await browser.close();
})();
Two rules keep this working. Keep the server value to scheme, host, and port only. And if your password contains characters like @ or :, the object form is exactly why you don't need URL-encoding tricks; pass the value literally.
This shape has been stable for years. The earliest "it doesn't work" reports, like playwright-python#171 from August 2020, turned out to be a deprecation warning plus an outdated Playwright; the reporter upgraded and closed the issue themselves. A more recent report, Playwright authentication bug #37444, claimed Chromium sent a blank Proxy-Authorization header while WebKit worked, but a community member could not reproduce it against a working Squid and the maintainers closed it the same day as not reproducible. So if Fix 1 fails for you in mid-2026, suspect your server string or an old Playwright version before suspecting the library.
If you're coming from Puppeteer, the equivalent there is launching with a bare --proxy-server=host:port and then calling page.authenticate({ username, password }), which enables request interception behind the scenes to answer the challenge. Playwright does the same job declaratively at launch, which is one less per-page call to forget.
Fix 2: one proxy per context for multi-identity work
A launch-level proxy means every page in the browser shares one egress IP. For account management or ad verification, where sessions must not bleed into each other, Playwright lets you set the proxy per browser context instead, also covered in the network docs. A context is an isolated profile with its own cookies and storage, and with this option, its own network identity.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const alice = await browser.newContext({
proxy: {
server: 'http://198.51.100.10:8080',
username: 'user-alice',
password: 'pass-alice',
},
});
const bob = await browser.newContext({
proxy: {
server: 'http://198.51.100.24:8080',
username: 'user-bob',
password: 'pass-bob',
},
});
const pageA = await alice.newPage();
const pageB = await bob.newPage();
await pageA.goto('https://api.ipify.org');
await pageB.goto('https://api.ipify.org');
console.log('alice:', await pageA.textContent('body'));
console.log('bob:', await pageB.textContent('body'));
await browser.close();
})();
One browser process, two contexts, two exit IPs, zero shared state. This pattern is where static IPs earn their keep. If each context maps to one fixed address, the identity that context builds, meaning its cookies plus its IP plus its session history, stays coherent across the whole run and across runs on different days. Sites that punish IP churn mid-session never see any. We walked through the workflow side of this pattern, including storage-state reuse, in our guide to running Playwright on ISP proxies.
A practical note on scale: contexts are cheap compared to browser processes, so ten identities is ten contexts in one Chromium, not ten Chromiums.
Fix 3: a local forwarder when the tool only takes a bare URL
Some tooling wrapped around Playwright or raw Chromium exposes a single proxy URL field with no separate credential inputs. CLI wrappers and older test harnesses are the usual offenders. Since Chromium will discard credentials in that URL, run a tiny forwarder on localhost that accepts unauthenticated connections and injects the auth upstream. The proxy-chain package does this in a few lines:
// npm i proxy-chain
const proxyChain = require('proxy-chain');
(async () => {
const upstream = 'http://your-username:your-password@proxy.example.com:8080';
// Starts a local proxy on a free port; no auth required locally.
const localUrl = await proxyChain.anonymizeProxy(upstream);
console.log(localUrl); // e.g. http://127.0.0.1:53999
// Hand localUrl to any tool that only accepts a bare proxy URL.
// When the job is done:
// await proxyChain.closeAnonymizedProxy(localUrl, true);
})();
The forwarder holds the credentials and answers the upstream 407 challenge, so Chromium talks to 127.0.0.1 with no authentication at all. Two cautions. Keep it bound to localhost, because anything that can reach that port can spend your proxy bandwidth under your account. And prefer Fix 1 or Fix 2 whenever you control the launch options, since an extra hop is one more process to babysit.
The SOCKS5 catch: no auth, full stop
Playwright supports plain socks5://host:port, but SOCKS5 with a username and password does not work in any browser Playwright drives. That's an open feature request, Support socks5 proxy with authentication (#10567), filed in November 2021 and still sitting in "collecting feedback" as of July 2026, in part because Chromium itself does not implement SOCKS5 authentication. The launch API reference is consistent: the username and password fields are documented for HTTP proxy authentication.
The practical consequence: if your provider only hands you SOCKS5 credentials, nothing in this post will save you. You need an HTTP(S) endpoint with username:password auth, which is the one configuration Playwright supports end to end. It's also, not coincidentally, the only auth scheme Stat Proxies sells: no SOCKS5, no IP whitelist, no connection tokens, just HTTP/HTTPS with username and password, which drops straight into the proxy object from Fix 1.
Where a static IP fits, and where it does not
Everything above applies to any authenticated HTTP proxy. Our stake in it: Stat Proxies sells static US ISP proxies announced from Tier-1 US carrier networks, flat per-IP pricing from $2.50 a month with unlimited bandwidth, so a Playwright session that pulls a few gigabytes of pages doesn't run up a per-GB meter. When you need a different address mid-project, IP replacement is programmatic through our management API rather than through connection-level rotation.
Honest scoping, because static ISP is not the answer to everything. If you're working a hostile anti-bot target that fingerprints and burns addresses aggressively, a rotating residential pool will outlast a handful of static IPs, and we don't sell rotation. Our IPs are US-only, so non-US geo work rules us out entirely. And no proxy of any kind fixes a JS or TLS fingerprint problem; if the site is blocking headless Chromium itself, changing the egress IP changes nothing.
Where the static model wins is exactly the Fix 2 pattern: one context, one IP, one identity that persists. Long-lived logins, saved storage state, and nightly scheduled jobs all behave better when the site sees the same ISP-grade address on Tuesday that it saw on Monday.
FAQ
Why does my proxy work in curl but fail in Playwright?
curl parses credentials out of the proxy URL and sends the Proxy-Authorization header itself. Chromium deliberately ignores credentials embedded in a proxy URL, so the same string that works in curl reaches the proxy with no authentication when Playwright drives Chromium. Passing username and password as separate fields in Playwright's proxy option fixes it.
Does Playwright support SOCKS5 proxies with authentication?
No. Plain unauthenticated SOCKS5 works, but username and password auth on SOCKS5 has been an open feature request since November 2021, largely because Chromium itself lacks SOCKS5 auth support. Use an HTTP or HTTPS proxy with username and password instead.
Can different pages in one Playwright browser use different proxies?
Yes, at the context level. Each browser context accepts its own proxy object with its own server and credentials, and contexts are isolated from one another, so a single browser process can hold several network identities at once. Pages inside the same context share that context's proxy.
Why does my proxy log show no Proxy-Authorization header from Playwright?
Chromium authenticates reactively. The first request goes out without the header, the proxy answers 407 Proxy Authentication Required, and only then does the browser retry with credentials attached. A proxy or upstream filter that requires the header preemptively on the first request will log it as missing even though your Playwright configuration is correct.