Puppeteer proxy setup

How to use proxies in Puppeteer

Wire up --proxy-server and page.authenticate, read 407s correctly, and rotate exits without Chromium quietly reusing old connections.

Field notes Setup checks Updated 2026-06-12

Use --proxy-server for the proxy host

Puppeteer will not pick up a proxy just because a URL sits somewhere in your config. Chromium needs the proxy host as a launch argument, and the credentials travel separately: the host and port go into --proxy-server, while the username and password go to page.authenticate() before the first navigation.

import puppeteer from 'puppeteer';
                        
                        const browser = await puppeteer.launch({
                          args: ['--proxy-server=http://proxynade.net:2555']
                        });
                        
                        const page = await browser.newPage();
                        await page.authenticate({
                          username: process.env.PROXY_USER,
                          password: process.env.PROXY_PASS
                        });
                        
                        await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

Avoid embedding user:pass@ in the launch argument. Chromium ignores credentials placed there, and the string still leaks into process lists and shell history.

On a Proxynade pool, PROXY_USER is the expanded username that carries the routing options, for example rt97db6958d9-plan-volume-country-us-lifetime-30: the base username, a required plan token (volume, premium, or datacenter), an optional lowercase country code, and an optional rotation lifetime in minutes. On datacenter, the lifetime token applies only to a sticky session, and the password goes in unchanged.

Separate 407 from target blocking

A 407 Proxy Authentication Required comes from the proxy itself, while a 403, a captcha, or an empty search result comes from the target. Keeping those two sources separate saves more debugging time than any other habit on this page.

ResultWhat it meansNext check
407Credentials failed at the proxyCheck username, password, account balance, and special characters.
Navigation timeoutTarget, network, or proxy route stalledTest a small HTML endpoint before loading a heavy page.
403The proxy connected, but target refusedChange headers, pacing, session age, or target policy.
Same IP every requestBrowser reused the same tunnelRestart the browser or create a new proxy assignment.

HTTP proxies, HTTPS targets, and the SOCKS5 trap

An http://host:port proxy line carries HTTPS targets fine: Chromium opens a CONNECT tunnel through the proxy and runs TLS to the site inside it, so there is no separate "https proxy" to buy and nothing to change for https:// URLs. The scheme in --proxy-server describes how the browser talks to the proxy, not which sites it can load through it.

SOCKS5 is the one to watch in Puppeteer. Chromium routes through socks5://host:port, but it never sends credentials to a SOCKS proxy, and page.authenticate() does not change that. On an authenticated gateway like a Proxynade pool line, use the http://proxynade.net:2555 form and keep the credentials in page.authenticate().

ErrorWhat Chromium is sayingFix
ERR_PROXY_CONNECTION_FAILEDCould not reach the proxy host at allCheck host, port, and firewall, and confirm the launch arg actually made it into args.
ERR_TUNNEL_CONNECTION_FAILEDProxy refused the CONNECT to the targetUsually auth or balance on the proxy side. Run the health check below before blaming the target.
ERR_NO_SUPPORTED_PROXIESMalformed or unsupported proxy URLUse http://host:port or socks5://host:port and nothing else in the string.
ERR_SOCKS_CONNECTION_FAILEDSOCKS handshake failedIf the gateway needs credentials, switch to the http form: Chromium will not auth to SOCKS.

Rotation needs process boundaries

Swapping the proxy string in memory does not guarantee a new exit, because Chromium keeps connections alive and reuses them. For strict per-task rotation, start a fresh browser with a fresh proxy assignment, and for sticky sessions keep the same browser open until the account flow is done. On a Proxynade pool the sticky side lives in the username: the lifetime-<minutes> token sets the rotation window, so a login flow that needs half an hour on one exit would use lifetime-30.

If the goal is only lower bandwidth rather than a new exit, block the obvious waste once the page loads correctly.

await page.setRequestInterception(true);
                        
                        page.on('request', request => {
                          const type = request.resourceType();
                          if (['image', 'font', 'media'].includes(type)) {
                            return request.abort();
                          }
                          return request.continue();
                        });

One side effect worth testing: page.authenticate() turns on request interception internally, which can affect throughput (see Puppeteer: network interception). Benchmark with the same auth path you run in production rather than an unauthenticated shortcut.

The proxy dashboard sees more than Puppeteer

Puppeteer reports what your page code observes, while the proxy meter sees the lower-level traffic underneath it: redirect chains, failed TLS attempts, blocked HTML, font files, and retries all count. That gap is why app-level counters usually look cleaner than the bandwidth bill.

A minimal Puppeteer proxy health check

Before running the real target, run one small navigation through the same browser shape you will use for the real work. A health check made with a different HTTP library only proves the credentials, not Puppeteer's launch wiring.

async function proxyHealthCheck(page) {
                          const started = Date.now();
                          const response = await page.goto('https://example.com', {
                            waitUntil: 'domcontentloaded',
                            timeout: 30000
                          });
                        
                          return {
                            status: response?.status(),
                            finalUrl: page.url(),
                            ms: Date.now() - started
                          };
                        }

A 407 here means credentials. A timeout points at the route or the target, so try a smaller page next. A target block means Puppeteer reached the site, and the proxy setup is no longer the main question.

When to restart the browser

Restart for hard proxy rotation, major credential changes, and any test where you must prove a new exit, and keep the browser alive for the length of one sticky account flow. Mixing those two goals causes the classic bug where the code says it rotated while Chromium quietly kept using an old connection.

For queue workers, the cleanest model treats the browser as part of the proxy assignment: a task gets a proxy label, a browser instance, and a retry budget, and the browser closes when the task ends. That costs some startup time, but it makes rotation honest.

Puppeteer proxy FAQ

Where do proxy credentials go in Puppeteer? Put the proxy host in --proxy-server and the credentials in page.authenticate() before the first navigation.

Can Puppeteer rotate proxies per request? Not cleanly at the browser tunnel level. For hard rotation, use a fresh browser or a fresh isolated task boundary.

Why does Puppeteer still use the old proxy? Chromium keeps sockets open and reuses them, so close the browser between hard rotations.

Why does auth slow down Puppeteer? The authentication path uses request interception behind the scenes, which can affect throughput.

Does an http:// proxy work for HTTPS sites in Puppeteer? Yes. Chromium opens a CONNECT tunnel through the proxy and runs TLS inside it, so one http://host:port line covers both http and https targets.

Can Puppeteer use SOCKS5 with a username and password? No. Chromium never sends credentials to SOCKS proxies, so an authenticated gateway needs the http form plus page.authenticate().

Production checks

  • Keep credentials out of CLI args.
  • Use one browser per hard proxy rotation.
  • Set timeouts per navigation.
  • Log status code and proxy label together.
  • Stop retrying when the response is a stable block.