NewUnlimited residential proxies: unmetered traffic on a fixed price. From $33.25/day

Puppeteer and Playwright Proxies: Auth, Rotation and Geo-Targeting

Node.js code for authenticated proxies in Puppeteer and Playwright, one IP per browser context, geo-targeting with matching locale and timezone, and fixes for common errors.

By the Proxonym team

Chromium does not accept proxy credentials inside a URL, so a Puppeteer proxy takes two steps: pass --proxy-server when you launch the browser, then call page.authenticate() with the username and password. Playwright folds both into a proxy option that you can set for the whole browser or for each context. This guide shows working code for both, one IP per browser context, geo-targeting, and the settings that keep a headless session consistent with its IP.

Puppeteer: launch args and page.authenticate#

JavaScript
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    args: ['--proxy-server=http://gw.proxonym.com:8000'],
  });
  const page = await browser.newPage();
  await page.authenticate({
    username: 'USERNAME-country-us-session-k3p9x2-lifetime-15',
    password: 'PASSWORD',
  });
  await page.goto('https://api.ipify.org', { waitUntil: 'domcontentloaded' });
  console.log(await page.evaluate(() => document.body.innerText));   // the exit IP
  await browser.close();
})();

Call page.authenticate() before the first navigation; it answers the gateway's 407 challenge for every request the page makes. Use the HTTP port, 8000: Chromium cannot authenticate to SOCKS5 proxies.

Note the session parameter in the username. A single page load pulls dozens of resources: scripts, styles, fonts and API calls. Without a sticky session, those connections could exit from different IPs, which no real visitor does and which breaks sites that tie cookies to an IP. Give every browser identity a sticky session that outlasts the job, up to 120 minutes on residential proxies. Rotating vs sticky sessions covers how to size it.

One IP per browser context#

Launching a browser per IP is slow. Browser contexts are cheap, isolated profiles inside one browser process, each with its own cookies, storage and cache. Pair each context with its own sticky session and you get many independent identities from a single launch.

Puppeteer#

JavaScript
const { randomBytes } = require('node:crypto');

async function withIdentity(browser, country, task) {
  const context = await browser.createBrowserContext({
    proxyServer: 'http://gw.proxonym.com:8000',
  });
  const page = await context.newPage();
  await page.authenticate({
    username: `USERNAME-country-${country}-session-${randomBytes(4).toString('hex')}-lifetime-20`,
    password: 'PASSWORD',
  });
  try {
    return await task(page);
  } finally {
    await context.close();
  }
}

// await withIdentity(browser, 'de', (page) => page.goto('https://example.com'));

createBrowserContext() is the name in Puppeteer 22 and later; older versions call it createIncognitoBrowserContext(). Keep one identity per context rather than switching usernames on an existing page: Chromium caches proxy credentials, so a second authenticate() call in the same context may not take effect.

Playwright#

JavaScript
const { chromium } = require('playwright');
const { randomBytes } = require('node:crypto');

(async () => {
  const browser = await chromium.launch();
  for (const country of ['us', 'gb', 'jp']) {
    const context = await browser.newContext({
      proxy: {
        server: 'http://gw.proxonym.com:8000',
        username: `USERNAME-country-${country}-session-${randomBytes(4).toString('hex')}-lifetime-20`,
        password: 'PASSWORD',
      },
    });
    const page = await context.newPage();
    await page.goto('https://api.ipify.org');
    console.log(country, await page.textContent('body'));
    await context.close();
  }
  await browser.close();
})();

To use one proxy for everything, pass the same proxy object to chromium.launch() instead. On older Playwright releases, Chromium on Windows needed a launch-level placeholder, proxy: { server: 'http://per-context' }, before per-context proxies worked. The Python API takes the same dictionary:

Python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context(proxy={
        "server": "http://gw.proxonym.com:8000",
        "username": "USERNAME-country-gb-session-q7m2x9-lifetime-20",
        "password": "PASSWORD",
    })
    page = context.new_page()
    page.goto("https://api.ipify.org")
    print(page.text_content("body"))
    browser.close()

Which proxy type to pair with a browser#

  • Rotating residential with sticky sessions for scraping and short-lived sessions: a fresh identity per context, each lasting up to 120 minutes.
  • ISP proxies for persistent profiles, such as logged-in accounts that should return from the same IP every day. The IP stays static for your whole term, and bandwidth is unlimited.
  • Unlimited residential for heavy, continuous rendering, where per-GB billing would otherwise dominate the cost.

Geo-targeting with a consistent profile#

Pick the exit location in the username with -country-, and add -state- or -city- for finer targeting. Then make the browser agree with it. A German IP paired with an American timezone and an English-only Accept-Language header is exactly the kind of inconsistency anti-bot scripts check for.

JavaScript
// Playwright: everything goes into the context options
const context = await browser.newContext({
  proxy: {
    server: 'http://gw.proxonym.com:8000',
    username: 'USERNAME-country-de-city-berlin-session-b81f2c-lifetime-30',
    password: 'PASSWORD',
  },
  locale: 'de-DE',
  timezoneId: 'Europe/Berlin',
});

// Puppeteer: set per page
await page.emulateTimezone('Europe/Berlin');
await page.setExtraHTTPHeaders({ 'Accept-Language': 'de-DE,de;q=0.9,en;q=0.6' });

Playwright's locale option sets both navigator.language and the Accept-Language header. In Puppeteer, the extra header does not change navigator.language, so check which of the two your target reads.

Check the profile before a run#

Confirm that the IP, timezone and language line up before you start a job:

JavaScript
await page.goto('https://api.ipify.org');
const report = await page.evaluate(() => ({
  ip: document.body.innerText.trim(),
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
  language: navigator.language,
}));
console.log(report);   // { ip: '...', timezone: 'Europe/Berlin', language: 'de-DE' }

Look the IP up in a geolocation database to confirm the country, and repeat the check for each context, since every context has its own session and therefore its own IP.

Headless detection: what a proxy fixes and what it does not#

A proxy changes the IP address. It does not change the browser. Sites that score automation look at both, so keep these points in mind:

  • Use the full headless mode. Puppeteer's default headless: true runs Chrome's new headless mode. In Playwright, pass channel: 'chromium' to use it instead of the lighter headless shell, which differs more from a regular browser.
  • Close the WebRTC leak. WebRTC can reveal your real IP over UDP, outside the proxy. Add --force-webrtc-ip-handling-policy=disable_non_proxied_udp to the launch args.
  • Stay consistent. Keep the same IP, user agent, timezone and cookies for the life of an identity. Randomizing the fingerprint on every request looks less human, not more.
  • Pace yourself. Wait for pages to load, avoid hammering one site from many contexts at once, and slow down when CAPTCHAs appear instead of pushing through them. Web scraping without getting blocked covers rate control in depth.

Browser traffic adds up quickly on per-GB plans. Blocking images, media and fonts cuts the transfer per page:

JavaScript
// Playwright
await context.route('**/*', (route) =>
  ['image', 'media', 'font'].includes(route.request().resourceType()) ? route.abort() : route.continue());

// Puppeteer
await page.setRequestInterception(true);
page.on('request', (req) =>
  ['image', 'media', 'font'].includes(req.resourceType()) ? req.abort() : req.continue());

For fleets of browsers rendering full pages around the clock, unlimited residential proxies bill by bandwidth instead of by GB and allow sticky sessions of up to 12 hours.

Troubleshooting#

ErrorLikely causeFix
net::ERR_PROXY_CONNECTION_FAILEDThe browser cannot reach the gatewayCheck the host and port; allow outbound traffic to port 8000
net::ERR_TUNNEL_CONNECTION_FAILEDThe gateway refused the CONNECT, for example with 402 (no balance), 403 (blocked target) or 502 (peer failed)Reproduce with curl to read the status code
net::ERR_INVALID_AUTH_CREDENTIALSWrong username or password, or authenticate() called after navigationAuthenticate first and copy credentials from the dashboard
Navigation timeoutA slow peer or a heavy pageUse waitUntil: 'domcontentloaded', block media, retry with a new session

To see exactly what the gateway answers, run the same username through curl with -v:

Shell
curl -v -o /dev/null -x http://gw.proxonym.com:8000 \
  -U "USERNAME-country-us-session-k3p9x2:PASSWORD" https://example.com

Ready-made snippets are on the Puppeteer and Playwright integration pages.

Key takeaways#

  • Puppeteer needs --proxy-server at launch (or proxyServer per context) plus page.authenticate(). Playwright takes one proxy option with server, username and password.
  • Always use a sticky session for browser traffic, one per identity.
  • Use one browser context per identity rather than one browser per IP.
  • Match locale and timezone to the exit country, and close the WebRTC leak.
  • Chromium cannot authenticate to SOCKS5, so use the HTTP port for authenticated proxies.

One gateway. Millions of identities.

Pick a plan, pay with crypto and send your first request through Proxonym in minutes.

  • Pay as you go
  • Instant activation
  • Crypto accepted
  • 24/7 support