Integration · Browser automation
How to use a proxy with Puppeteer
Puppeteer sets a proxy in two parts: the address goes to Chrome as --proxy-server (or proxyServer on a browser context), and the username and password go to page.authenticate(). Chrome's proxy flag has no place for credentials, so skipping the second call is a common reason a Puppeteer proxy "does not work".
Reviewed 30 Oct 2026
http://USERNAME _pool-arcfast _country-de:PASSWORD@ proxy.arcproxies.io:8000The short answer
--proxy-servertakes host and port only; credentials always go throughpage.authenticate().browser.createBrowserContext({ proxyServer })gives each context its own proxy, so one browser can run many sticky sessions at once.authenticate()is per page: every new page, including popups, needs it before its first navigation.- Rejected credentials come back as a 407 response from
goto(), not as an exception, so check the status.
Setup
Set up Puppeteer with a proxy, step by step
- 01
Install Puppeteer
puppeteerdownloads a compatible Chrome on install;puppeteer-coredoes not and needsexecutablePath. The proxy API is the same in both.shell1 linenpm i puppeteer - 02
Launch through the gateway, authenticate, check the exit IP
Pass the gateway as a launch argument, then call
page.authenticate()before the firstgoto(). The IP echo page shows the exit the target will see.check-exit.mjs11 linesimport puppeteer from 'puppeteer';const browser = await puppeteer.launch({ args: ['--proxy-server=http://proxy.arcproxies.io:8000'], // host and port only});const page = await browser.newPage();await page.authenticate({ username: 'USERNAME_pool-arcfast_country-de_session-k3f9a1x2_sesstime-10', password: 'PASSWORD' });const res = await page.goto('https://api.ipify.org?format=json');console.log(res.status(), await page.evaluate(() => document.body.innerText));await browser.close(); - 03
One browser context per task, each with its own exit
Launch once without a proxy flag, then open a context per task with
proxyServerand authenticate its page with a fresh_session-handle. Contexts do not share cookies or cache, so each task looks like one visitor on one IP.contexts.mjs26 linesimport puppeteer from 'puppeteer';import { randomBytes } from 'node:crypto';const BASE = 'USERNAME_pool-arcfast_country-de'; // pool + country; the session is added per taskconst browser = await puppeteer.launch();async function runTask(url) { const handle = randomBytes(4).toString('hex'); // new handle, new exit IP const context = await browser.createBrowserContext({ proxyServer: 'http://proxy.arcproxies.io:8000', }); try { const page = await context.newPage(); await page.authenticate({ username: `${BASE}_session-${handle}_sesstime-10`, password: 'PASSWORD', }); const res = await page.goto(url, { waitUntil: 'domcontentloaded' }); return { url, status: res?.status(), title: await page.title() }; } finally { await context.close(); }}console.log(await Promise.all(['https://example.com/a', 'https://example.com/b'].map(runTask)));await browser.close(); - 04
Pick a country or city per context
Targeting is the username, so a context per market is simply a different username for
authenticate(). Country codes are lowercase ISO; cities come from the published list, lowercased without spaces.markets.mjs14 linesconst MARKETS = { 'de-munich': 'USERNAME_pool-arcfast_country-de_city-munich', 'gb-london': 'USERNAME_pool-arcfast_country-gb_city-london', 'jp-tokyo': 'USERNAME_pool-arcfast_country-jp_city-tokyo',};for (const [label, username] of Object.entries(MARKETS)) { const context = await browser.createBrowserContext({ proxyServer: 'http://proxy.arcproxies.io:8000' }); const page = await context.newPage(); await page.authenticate({ username, password: 'PASSWORD' }); await page.goto('https://example.com/pricing'); console.log(label, await page.$eval('h1', (el) => el.textContent)); await context.close();} - 05
Treat 407, 429 and net:: errors differently
A wrong password returns a 407 response that will not improve with retries, so stop. A 429 or 5xx is worth a retry after a pause on a new exit. Tunnel failures and timeouts throw; retry those on a new handle too.
retry.mjs28 linesimport { TimeoutError } from 'puppeteer';import { randomBytes } from 'node:crypto';import { setTimeout as sleep } from 'node:timers/promises';async function fetchHtml(browser, url, attempts = 3) { for (let attempt = 0; attempt < attempts; attempt++) { const context = await browser.createBrowserContext({ proxyServer: 'http://proxy.arcproxies.io:8000' }); try { const page = await context.newPage(); await page.authenticate({ username: `USERNAME_pool-arcfast_country-de_session-${randomBytes(4).toString('hex')}_sesstime-10`, password: 'PASSWORD', }); const res = await page.goto(url, { timeout: 30_000 }); const status = res?.status() ?? 0; if (status === 407) throw new Error('407: check the proxy username and password'); if (status === 429 || status >= 500) { await sleep(5000 * 2 ** attempt); continue; } return await page.content(); } catch (err) { const retryable = err instanceof TimeoutError || String(err.message).includes('net::ERR_'); if (!retryable) throw err; await sleep(1000 * 2 ** attempt); } finally { await context.close(); } } throw new Error(`gave up on ${url}`);}
Why Puppeteer splits the proxy address and the credentials
Chrome reads its proxy from the command line or from the context settings, and neither has a field for a username or password. Puppeteer's BrowserContextOptions.proxyServer documentation says so directly: username and password "can be set in Page.authenticate".
page.authenticate() answers the proxy's authentication challenge for every request that page makes. Puppeteer implements it with request interception, which its docs note "might affect performance". If you also call page.setRequestInterception(true) to block images or fonts, check request.isInterceptResolutionHandled() in your handler so the two do not both try to resolve the same request.
Rotating vs sticky sessions in Puppeteer
A rotating username lets each new connection take a different exit. In a browser that can split one page across several IPs, because a page load opens many connections. Use a sticky handle per context: {{USER}}_session-<handle>_sesstime-<minutes>, a new handle for the next task.
The hold is best effort. If an exit drops before _sesstime- runs out, the next connection gets a new IP; the retry pattern above handles that the same way as a timeout. Rotating vs sticky sessions explains when each one fits.
HTTPS targets and SOCKS5
- HTTPS sites go through an
http://proxy via an HTTP CONNECT tunnel: TLS runs end to end between Chrome and the site. - The gateway also speaks SOCKS5 on the same port, but
page.authenticate()is documented for HTTP authentication. Keephttp://in--proxy-serverfor authenticated sessions. proxyBypassListon a context lists hosts that should skip the proxy.
The problem
Common pitfalls
Credentials in --proxy-server
--proxy-server=http://user:pass@host:port does not authenticate. Pass host and port only, and the credentials to page.authenticate().
Pages that never called authenticate()
Popups and tabs a site opens are new pages without credentials. Listen for targetcreated (or the context's targetcreated) and authenticate those pages too, or block popups.
Assuming a failed proxy throws
Rejected credentials resolve goto() with a 407 response and Chrome's error page. Code that only catches exceptions will parse that error page as content.
One proxy for every parallel page
A launch-level --proxy-server with one sticky username puts every page on the same exit IP. For parallel work, use a context per task with its own handle.
FAQ
Questions
Something not covered? Write to contact@arcproxies.io.
How do I use a proxy with a username and password in Puppeteer?
Launch with args: ['--proxy-server=http://host:port'], then call await page.authenticate({ username, password }) on each page before its first navigation. Chrome's proxy flag cannot carry credentials, so authenticate() is the documented way to supply them.
Can Puppeteer use a different proxy per page?
Per browser context, yes: browser.createBrowserContext({ proxyServer: 'http://host:port' }). Pages in that context use that proxy. With ARC the address is the same for every context; a different _session- or _country- token in each context's username is what gives each one its own exit.
Why does Puppeteer return status 407?
407 Proxy Authentication Required means the gateway rejected the credentials, or none were sent. Make sure page.authenticate() ran on that page before goto() and that the username and password are copied exactly from the dashboard. Retrying will not fix it.
How do I rotate proxies in Puppeteer?
Open a new browser context with a new _session- handle in the username for each task, and close it when the task ends. Rotating inside one page is rarely what you want, because a single page load spans many connections.
Sources & review
Last reviewed by the ARC Proxies team. Prices are read from the live catalog at build time.
- [1]Puppeteer: Page.authenticate() method
- [2]Puppeteer: BrowserContextOptions interface
- [3]Puppeteer: Browser.createBrowserContext() method
Next
What people build with it
Integrations
Other browser automation
Get started
Get credentials and paste them in.
Sign up, buy a gigabyte, generate a username in the dashboard. Code WELCOME15 takes 15% off your first purchase; bandwidth never expires.