15% off your first purchase with code WELCOME15, up to 25 GB.

See pricing

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

your proxy, as a URL
http://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000

The short answer

  • --proxy-server takes host and port only; credentials always go through page.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

  1. 01

    Install Puppeteer

    puppeteer downloads a compatible Chrome on install; puppeteer-core does not and needs executablePath. The proxy API is the same in both.

    shell
    npm i puppeteer
  2. 02

    Launch through the gateway, authenticate, check the exit IP

    Pass the gateway as a launch argument, then call page.authenticate() before the first goto(). The IP echo page shows the exit the target will see.

    check-exit.mjs
    import 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();
  3. 03

    One browser context per task, each with its own exit

    Launch once without a proxy flag, then open a context per task with proxyServer and 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.mjs
    import 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();
  4. 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.mjs
    const 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();}
  5. 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.mjs
    import { 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. Keep http:// in --proxy-server for authenticated sessions.
  • proxyBypassList on 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. [1]Puppeteer: Page.authenticate() method
  2. [2]Puppeteer: BrowserContextOptions interface
  3. [3]Puppeteer: Browser.createBrowserContext() method

Next

What people build with it

Use caseProxies for web scraping and data collectionA second provider that just works — Scrapy, requests, Crawlee or Playwright, self-serve, no sales call, bandwidth that never expires.Use caseProxies for price and competitor monitoringSee the price a shopper in Berlin, Lyon or Chicago sees. ISP bandwidth at our lowest price per GB for HTML checks, residential where a marketplace is strict.Use caseProxies for QA, app and localisation testingSee your own site the way a user in Tokyo, São Paulo or Munich sees it — currency, language, consent banner, CDN edge and all — before they report it.GlossarySticky sessionA sticky session is a setting on a rotating proxy that keeps the same exit IP address for a chosen period, usually by attaching a session identifier to the connection, so a multi-step task appears to come from one address from start to finish.GlossaryHTTP CONNECTHTTP CONNECT is the HTTP request method a client sends to a proxy to ask for a tunnel to a host and port; once the proxy answers with a 2xx status it blindly forwards bytes in both directions, which is how HTTPS passes through an HTTP proxy with TLS intact end to end (RFC 9110, section 9.3.6).GlossarySOCKS5SOCKS5 is a proxy protocol, standardised in RFC 1928 in 1996, that relays TCP connections and UDP datagrams between a client and a destination without interpreting the application traffic, with username/password authentication defined separately in RFC 1929.GuideRotating vs sticky sessionsRotating proxies change exit IP per connection; sticky sessions hold one IP for a set time. How each works, when to use which, and how to set them up.IntegrationPlaywrightSet up an authenticated proxy in Playwright for Python and Node: launch and per-context proxies, sticky sessions, geo-targeting and handling proxy errors.

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.