Puppeteer Proxies: Why Chromium Ignores Your Password, and What to Do About It
Chromium's `--proxy-server` flag does not accept credentials, and it never has. Putting `user:pass@` in front of the host does not fail loudly — the userinfo is simply discarded, the request goes out unauthenticated, and the proxy answers 407. Puppeteer's `page.authenticate()` solves this for a single identity, and understanding why it does not solve it for several is the difference between a demo and a working crawler.
- puppeteer
- automation
- browser automation
- 407

Table of contents
If you only need the working setup, the Puppeteer integration page has it. This article is about the constraint underneath it, because that constraint determines your architecture rather than just your configuration.
What actually happens
// Looks reasonable. Chromium throws the credentials away.
const browser = await puppeteer.launch({
args: ["--proxy-server=http://USERNAME:PASSWORD@PROXY_HOST:PROXY_PORT"],
});Chromium parses that flag as a host and port and ignores everything before the @. The first navigation reaches the proxy without a Proxy-Authorization header, the proxy replies with a 407 challenge, and Chromium — having no credentials to offer — renders an error page. Nothing in the launch call warned you.
const browser = await puppeteer.launch({
args: ["--proxy-server=http://PROXY_HOST:PROXY_PORT"],
});
const page = await browser.newPage();
await page.authenticate({ username: "USERNAME", password: "PASSWORD" });
await page.goto("https://api.ipify.org?format=json");page.authenticate() is scoped to that page. Every new page needs its own call, and a page opened by target=_blank will not inherit it — a common cause of a crawl that works for the first result and fails on the rest.
The constraint that shapes everything else
The proxy is a browser-process launch flag, so it cannot be changed at runtime. Puppeteer has no equivalent of Playwright's per-context proxy: browser.createIncognitoBrowserContext() isolates cookies and storage, but every context in the process still exits through the same address.
| What you need | What it costs in Puppeteer |
|---|---|
| One exit, many pages | Nothing — the default case |
| Isolated cookies, same exit | An incognito context per identity |
| A different exit per identity | A separate browser process, or a local relay |
| Rotation mid-session | Restart the browser, or route through a relay |
Pattern 1: one browser per identity
const crypto = require("crypto");
async function browserWithFreshExit() {
const tag = crypto.randomBytes(4).toString("hex");
const browser = await puppeteer.launch({
args: ["--proxy-server=http://PROXY_HOST:PROXY_PORT"],
});
const page = await browser.newPage();
await page.authenticate({
username: `USERNAME-session-${tag}`,
password: "PASSWORD",
});
return { browser, page };
}Simple and correct, and the right answer for small concurrency. The cost is real though: a Chromium process is 100–300MB, so twenty identities is several gigabytes of RAM and twenty cold starts.
Pattern 2: a local relay
Point Chromium at http://127.0.0.1:PORT — a local proxy with no authentication — and let that process add the upstream credentials and choose the exit per connection. Chromium never learns that the exit changed, so one browser serves many identities without a restart.
- No credential handling in Chromium at all, so
page.authenticate()disappears from your code. - The exit can change per connection, driven by whatever logic you put in the relay.
- One browser process, so memory stays flat as concurrency grows.
- The relay is one more moving part, and it is now in the path of every request.
This is what most 'rotating proxy for Puppeteer' packages do internally. Knowing that is useful: it tells you where to look when one of them misbehaves.
Pattern 3: per-request routing
For collecting data rather than driving a UI, the browser does not need to fetch everything. Intercept requests and pull the ones you care about with a plain HTTP client through whichever exit you like, leaving Chromium to render.
await page.setRequestInterception(true);
page.on("request", (req) => {
if (["image", "font", "media"].includes(req.resourceType())) return req.abort();
return req.continue();
});Static exits, no rotation to manage
One stable residential-registered IP per worker means no relay, no restarts and no session bookkeeping. ISP proxies from €1.80/IP.
Verifying, and the headless tell
Confirm the exit from inside the browser, then confirm the browser does not announce itself. Old headless Chrome shipped HeadlessChrome in the User-Agent and left navigator.webdriver set to true; the modern headless mode fixed much of that, but only if you use it.
const browser = await puppeteer.launch({
headless: "new",
args: [
"--proxy-server=http://PROXY_HOST:PROXY_PORT",
"--disable-blink-features=AutomationControlled",
],
});Beyond that, resist the urge to spoof aggressively. A User-Agent that disagrees with the TLS handshake or the rendered feature set is a stronger signal than the default would have been — the reasoning is set out in how to avoid getting blocked.
Symptom to cause
| Symptom | Cause |
|---|---|
| 407 on first navigation | Credentials in --proxy-server, or authenticate() called too late |
| Works on page one, fails on page two | authenticate() not called on the new page |
ERR_TUNNEL_CONNECTION_FAILED | Wrong port, wrong protocol, or the exit refused CONNECT |
| Real IP shows through | Flag missing from args, or NO_PROXY excludes the host |
| Every identity shares one exit | Working as designed — the proxy is per process |
The whole subject reduces to one design decision: whether the browser or something in front of it owns the proxy. Puppeteer makes that decision for you at launch, which is precisely why it is worth making deliberately.
Frequently asked questions
Why does Puppeteer ignore my proxy username and password?
Chromium's --proxy-server flag accepts only a host and port. Anything before the @ is discarded without warning, so the request goes out unauthenticated and the proxy responds with 407. Use page.authenticate() before the first navigation instead.
Can I use a different proxy per page in Puppeteer?
Not directly. The proxy is a launch argument for the whole browser process, and incognito contexts isolate cookies but share the exit. Run one browser per identity, or point Chromium at a local relay that picks the upstream exit per connection.
How do I rotate proxies in Puppeteer without restarting the browser?
Route through an unauthenticated local proxy that holds the upstream credentials and selects the exit itself. Chromium keeps talking to 127.0.0.1 while the relay changes the exit underneath, which is how most rotating-proxy plugins work internally.
Does page.authenticate work for popups and new tabs?
No. It applies to the page you called it on. A page opened by target=_blank or window.open starts without it, which is why crawls often succeed on the first result and fail on everything opened from it.
What causes ERR_TUNNEL_CONNECTION_FAILED with a proxy?
The CONNECT request to the proxy failed. The usual causes are the wrong port for the protocol, a SOCKS endpoint addressed as HTTP, credentials that were never sent, or an exit that refused the tunnel.
Is Puppeteer or Playwright better for proxy work?
Playwright, if you need several exits at once, because it supports a proxy per browser context and handles credentials natively. Puppeteer is perfectly adequate when one process needs one exit.
The seamless team runs residential, ISP and datacenter proxy infrastructure and writes these guides from day-to-day operational experience.
Ready to try seamless proxies?
Residential, ISP and datacenter proxies with no data expiry.
Browse PlansKeep reading
Playwright Proxies: Authentication, Per-Context Rotation and Real Geo Testing
One browser, many identities: set the proxy per context, keep locale and timezone honest, and stop leaking the fact that your headless Chrome is headless.
Troubleshooting407 Proxy Authentication Required: Every Cause and Its Fix
The one proxy error that is entirely on your side of the connection — and the handful of clients that cause it even when your credentials are perfect.
GuidesHow to Avoid Getting Blocked When Web Scraping
The six layers a modern anti-bot system checks, in the order it checks them — and what to change at each one to stay unblocked.
