All posts

Puppeteer proxy setup: auth, SOCKS5 and rotation

How do you set a Puppeteer proxy? Use --proxy-server for the browser or proxyServer per context, and page.authenticate() for logins. Learn the exact code.

  • puppeteer
  • proxy
  • proxy authentication
  • socks5
  • web scraping
On this page

A Puppeteer proxy is set in one of three places: the --proxy-server flag in args, a local forwarder you run, or the puppeteer.connect() WebSocket URL of a hosted browser. Chrome ignores credentials inside the --proxy-server value, so HTTP proxy logins go through page.authenticate(). Wayfern’s hosted browser takes the exit in the connection URL instead of a launch flag.

Your scraper or test suite runs fine on your machine. Then it needs a different exit IP, so you paste in the first Puppeteer proxy snippet you find, and Chrome answers with a 407 or a URL it can’t parse.

Two mistakes recur in copied code: a forwarder URL shaped like http://user:pass@http://..., and a “per request” rotation that actually picks one proxy per launch. After reading, you can pick the right scope, authenticate correctly, rotate honestly and decide who holds the proxy.

Key Takeaways

  • Puppeteer scopes a proxy to the whole browser or one context. Per-page routing needs an interception package such as puppeteer-proxy.
  • Rotation only happens where you write it: a new IP per launch, context or request is code you own.
  • Keep one IP for a whole login flow, and rotate between flows, never inside one.
  • Wayfern refuses an unknown location with close code 4007 instead of falling back to another country.

How do you set a proxy in Puppeteer, and at what scope?

You set a Puppeteer proxy at launch for the whole browser, or per browser context with proxyServer; there is no per-page setting.

Image generated

The launch flag goes in args, and a context option overrides it for one context. A page opened in either one inherits that exit.

import puppeteer from "puppeteer";

// Browser-wide: every page in every context uses this proxy.
const browser = await puppeteer.launch({
  args: ["--proxy-server=http://host:port"],
});

// Per context: pages opened here use their own proxy.
const context = await browser.createBrowserContext({
  proxyServer: "http://other-host:port",
});
const page = await context.newPage();
await page.goto("https://api.ipify.org?format=json");
console.log(await page.evaluate(() => document.body.innerText));

If the printed IP is the proxy’s and not yours, the exit changed. Run the same check against the browser-wide proxy to see the difference.

ScopeHow you set itWhat it coversTrade-off
Browser-wide--proxy-server in args at launch()Every context and pageChanging the exit means launching a new browser
Per browser contextproxyServer in createBrowserContext()Pages opened in that contextCookies are isolated, so one browser needs one context per exit
Per pageNo native methodNothing built inInterception plugins add a hop

The Puppeteer API reference documents proxyServer as a browser context option, described as a “Proxy server with optional port to use for all requests.” The current method name is createBrowserContext.

Older code and tutorials use createIncognitoBrowserContext, so rename it when you copy.

The Page class in the same reference has no setProxy(). Any guide that shows a per-page proxy is using either a context per page or an interception package.

The puppeteer-proxy README says it “allows to change proxy per Page and per Request” and that it routes all traffic through Node.js.

That is the extra hop. Every request passes through your Node process before it reaches the proxy, which adds latency.

Treat it as a workaround, and reach for it only when a context-level proxyServer can’t express your case, such as changing the exit between requests on one page.

  • Browser-wide fits one exit for a whole job, such as a test run from a single location.
  • Per context fits several exits in one process, as long as each exit gets its own context.
  • Per page has no native option. Open a new context instead.

How do you authenticate a Puppeteer proxy with a username and password?

You authenticate a Puppeteer proxy by answering the proxy’s 407 challenge with page.authenticate(), by running a local forwarder such as proxy-chain, or by allowlisting your IP at the proxy.

Why the obvious attempt fails. Writing --proxy-server=http://user:pass@host:port sends the request with no login. RFC 9110 defines the reply: 407 “indicates that the client needs to authenticate itself in order to use a proxy for this request.” Nothing loads.

  1. Answer the challenge with page.authenticate(). Launch with a bare --proxy-server=http://host:port, then call await page.authenticate({ username, password }) before page.goto(). The Puppeteer API reference describes the method as “Provide credentials for HTTP authentication.” Called after navigation starts, it is too late: the 407 has already happened.

  2. Run a local forwarder. proxy-chain opens a local proxy that logs in upstream for you. Pass the upstream URL as http://user:pass@host:port, with one scheme and no second http://:

import puppeteer from "puppeteer";
import { anonymizeProxy, closeAnonymizedProxy } from "proxy-chain";

const upstreamProxyUrl = "http://user:[email protected]:8000";
const localProxyUrl = await anonymizeProxy(upstreamProxyUrl);

const browser = await puppeteer.launch({
  args: [`--proxy-server=${localProxyUrl}`],
});
try {
  const page = await browser.newPage();
  await page.goto("https://example.com");
} finally {
  await browser.close();
  await closeAnonymizedProxy(localProxyUrl, true);
}

Success is a page that loads through the proxy’s exit. Your machine holds the credentials here, in memory, so keep them in environment variables.

  1. Allowlist your IP. If your provider can trust your server’s address, no login is needed and the bare --proxy-server value works. This fails when your egress IP changes, such as on laptops or autoscaled workers.

Why header injection is unreliable. Some pages set Proxy-Authorization with page.setExtraHTTPHeaders() and call it solved. For HTTPS, the browser first asks the proxy to open a tunnel: RFC 9110 says CONNECT “requests that the recipient establish a tunnel to the destination origin server.”

Page-level headers belong to the requests inside that tunnel, so they don’t reach the proxy at the moment it decides. Treat snippets that rely on this as unsupported.

When the site also wants basic auth. page.authenticate() takes one username and password per page, and it answers whichever challenge arrives. A proxy login and a site’s basic-auth login compete for that single slot.

Use a forwarder or an allowlist for the proxy, and keep page.authenticate() for the site.

Does Chrome support SOCKS5 proxy credentials?

Chrome’s SOCKS5 support does not accept a username and password, so page.authenticate() cannot authenticate a SOCKS5 proxy. SOCKS5 itself defines username/password authentication in RFC 1929, but Chrome does not implement it.

The mismatch is in where the credentials travel. RFC 1929 describes a login step inside the SOCKS handshake, one of the protocols that fit the SOCKS V5 authentication “framework”. That exchange happens before any HTTP request is sent.

page.authenticate() provides credentials for HTTP authentication, and the SOCKS login is not HTTP. Chromium’s proxy documentation states it directly: “No authentication methods are supported for SOCKSv5 in Chrome (although some do exist for the protocol).”

Pick one of three routes:

  • IP-allowlisted SOCKS5. Add your machine’s egress IP at the provider, then launch with --proxy-server=socks5://host:1080. No credentials are involved, so nothing can be dropped.
  • HTTP proxy. Launch with --proxy-server=http://host:port and call page.authenticate({ username, password }) before page.goto().
  • Local forwarder. A tool such as proxy-chain accepts the credentials and presents Chrome with an unauthenticated local proxy. Use it only if its README documents your upstream scheme, and test one request before you rely on it.

An allowlist ties access to an IP address. If your scraper runs from machines whose addresses change, an HTTP proxy is the easier route.

How do you rotate proxies in Puppeteer without breaking sessions?

Rotating proxies in Puppeteer means changing the exit IP per browser launch, per browser context or per request, and you keep sessions intact by changing it only between flows, never inside one.

  • Per launch: put --proxy-server in the args of puppeteer.launch(). A new IP means a new browser process, which is the slowest option.
  • Per context: pass proxyServer when you create a browser context. One browser holds several contexts, each with its own cookie jar and its own exit.
  • Per request: Chrome has no setting for this. You need a rotating gateway on the provider side, where one endpoint hands each new connection a different exit, or an interception package. The puppeteer-proxy package is one such package.

The launch option and the context proxyServer are the only places where you code rotation yourself.

A common mistake. A script like this is often described as per-request rotation:

const proxies = ["http://p1.example:8000", "http://p2.example:8000"];
const pick = proxies[Math.floor(Math.random() * proxies.length)];
const browser = await puppeteer.launch({ args: [`--proxy-server=${pick}`] });
// Every request from this browser leaves through `pick`.

The random choice runs once, at launch. Chrome then uses that one proxy for every request until you close the browser, so you get one IP per run.

To get a different IP per context, call browser.createBrowserContext({ proxyServer: pick }) once for each context.

When to keep the IP fixed. A cookie issued to one IP and replayed from another is an easy signal for a site to read, as Wayfern’s anti-detect browser sessions page puts it. Logins, carts and checkouts should keep one IP from the first request to the last.

Give each login its own context and its own exit, then discard both when the flow ends.

With a rotating gateway, look for a sticky or session option, usually a session id in the proxy username. On a hosted browser, Wayfern’s session parameter holds one residential IP for a flow. Our guide to scraping behind logins shows the same idea end to end.

How do you choose a Puppeteer proxy exit with puppeteer.connect()?

You choose a Puppeteer proxy exit with puppeteer.connect() by putting the exit in the WebSocket URL, because a hosted browser is already running and you can’t pass --proxy-server to it.

  1. Mint a link token and keep it in an env var. The token is the only secret your script holds. Proxy credentials never appear in your code.
  2. Connect with the exit in the query string. Set browserWSEndpoint and add country (ISO-3166), region, city, isp or session as needed:
import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
  browserWSEndpoint:
    `wss://api.wayfern.com/ws?token=${process.env.WF_TOKEN}&country=US&region=CA&session=run42`,
});
const page = await browser.newPage();
await page.goto("https://example.com/login");
  1. Pick datacenter or residential on purpose. Wayfern’s pricing page says that with no location parameters you get the default datacenter exit, and its egress is free. Residential exits are metered by bandwidth, so use them only for targets that need a local, consumer-looking IP. Current rates are listed on the same page.
  2. Add session to any flow that spans pages. A sticky session id holds one residential IP for the flow, so the cookie and the IP that received it stay together. Change the id only between flows.
  3. Handle a refused location. A typo such as region=XX closes the socket with code 4007, as Wayfern’s anti-detect sessions page documents. You get an error, not a quiet exit in another country, so fix the parameter instead of retrying.
  4. Wait out the cold start. The Wayfern docs say to plan for about 45 to 50 seconds on the first connection, longer with a residential exit.

The URL above holds one California residential IP for the whole run42 flow. Per the anti-detect sessions page, cookies and storage live for as long as your socket is open, and a pooled instance is reused only for the same link token, workload and exit, so a different country gets a fresh instance.

On a hosted connection you can’t pick an exit with a launch flag. If you need per-context routing, launch locally and use proxyServer.

Can you bring your own proxy, and when is a local launch the better fit?

Yes, you can bring your own proxy: save it on a Wayfern link with proxyMode: "own", then pick it for a connection with proxy=own.

With puppeteer.connect() the browser is already running, so the launch flag isn’t yours to set. You choose the proxy on the connection instead:

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://api.wayfern.com/ws?token=${process.env.WF_LINK_TOKEN}&proxy=own`,
});

The Wayfern API reference lists http, https and socks5 for a saved proxy, and proxy takes own, residential or none.

The password is encrypted at rest and never goes in the URL or into Chrome, so your script holds no proxy credentials. Own-proxy traffic isn’t metered as residential bandwidth.

The same reference says a saved proxy must authenticate with a username and password (IP allowlists do not work there), and the gateway logs in to it before the browser starts, so a socks5 proxy with a password works on this route.

The limits, stated plainly:

  • No per-page proxy inside one connection. The exit is fixed when the socket opens.
  • No persistent profiles. The anti-detect sessions page says there are no persistent customer profiles, so save and restore cookies and storage in your own code between runs.
  • No mobile emulation. The browser presents a desktop device.
  • Chromium family only. Wayfern’s Browserbase comparison says it plainly: “No Firefox, no WebKit.”

Which route fits which situation:

  1. Local --proxy-server: one exit for the whole browser, local debugging, and full control of the binary.
  2. Context proxyServer: a different exit for each browser context in one process.
  3. Local forwarder: you launch Chrome yourself and the upstream proxy needs a username and password.
  4. Hosted exit: you want country, region, city or a sticky session chosen on the URL, with no proxy to buy.
  5. Hosted own proxy: you already pay for a proxy and want its IP on a hosted browser, with the credentials kept out of your code.

If you need a new exit for every context or page, a local launch is the better fit.

Frequently asked questions

How do I use a proxy in Puppeteer?

Pass --proxy-server=host:port in the args option of puppeteer.launch(). For a username and password, call page.authenticate() before navigation, because Chrome ignores credentials in the flag. For a hosted browser, call puppeteer.connect() with a WebSocket URL that selects the exit instead.

Why does my Puppeteer proxy return 407?

Your proxy sent a 407 challenge and nothing answered it. Call page.authenticate() before page.goto(). If the target site also uses basic auth, the two logins clash, so allowlist your IP at the proxy instead.

Can Puppeteer set a different proxy for each page?

Not natively. Set proxyServer per browser context, or use puppeteer-proxy, which changes the proxy per page and per request but routes traffic through Node.js. A hosted connection has no per-page proxy either.

Does Puppeteer work with a SOCKS5 proxy that needs a password?

Not directly. Chrome’s SOCKS5 support takes no credentials, so page.authenticate() can’t help. Allowlist your IP at the proxy, or save the proxy with its password on a Wayfern link (proxyMode: "own") and pick it with proxy=own.

How do I keep the same IP for a whole login flow?

Hold one exit from sign-in to the last request. On a hosted browser, add a residential exit and a session id to the WebSocket URL. A cookie issued to one IP and replayed from another is easy for a site to read, as Wayfern’s anti-detect sessions page notes, and our authenticated scraping guide shows the full flow.

What should you do next with your Puppeteer proxy setup?

Today: Decide who holds your Puppeteer proxy: your launch flags, a local forwarder, or a hosted browser. Then pick the scope: the whole browser or one context.

Replace any Proxy-Authorization header injection and any user:pass@host string inside --proxy-server with page.authenticate(), a forwarder or an IP allowlist.

This week: Decide your rotation level: per launch, per context or per request. Add a sticky exit to every login flow.

Then, if you want the exit chosen on the connection: create a link token for your project under Browser links. Connect with puppeteer.connect() to wss://api.wayfern.com/ws?token=LINK_TOKEN, and add country and session parameters for a residential exit held for one flow. No SDK is required, and no proxy credentials appear in your script.

The quickstart in the Wayfern docs walks through the first connection: create one link token and point a single puppeteer.connect() call at a page that echoes your IP.

Sources

Page.authenticate() method | Puppeteer · puppeteer-proxy - npm · Anti-Detect Browser Sessions over CDP | Wayfern · BrowserContextOptions interface | Puppeteer · RFC 9110: HTTP Semantics · RFC 1929: Username/Password Authentication for SOCKS V5 · Wayfern pricing · Wayfern API reference. Every other source is linked where the article cites it.

See us more often in Google

One click marks Wayfern as a preferred source in Google, so our posts can show up more often in your Top Stories, AI Mode and AI Overviews.