Skip to main content
[ DOCS / PUPPETEER ]

Puppeteer proxy authentication with a username and password

Chrome’s --proxy-server flag takes no login, which is where most Puppeteer proxy setups get stuck. Puppeteer answers it for you with page.authenticate(): two lines, and every page loads from a real home IP in the country you pick. No extension, no local forwarder, no SDK.

  • Two lines--proxy-server and page.authenticate()
  • One IP per contextmany identities in one browser
  • Real home IPs219 countries, down to the city

Puppeteer loads whole pages, so start small: 1 GB of Residential, $0.99, no subscription. Tested with Puppeteer 25.16 and Chrome — November 2026

KEY FACTS

  • Puppeteer takes the proxy as a Chrome flag, --proxy-server=http://gw.ludaxproxy.com:7777, and the login through page.authenticate({ username, password }) before the first goto(). The flag itself takes no username or password.
  • Each browser context can have its own proxy: browser.createBrowserContext({ proxyServer }), with page.authenticate() on its pages. Tested with Puppeteer 25.16 and Chrome.
  • A browser opens many connections per page. Without a session token each new connection can leave from a new IP, so give each context -session-<token>-time-<minutes>: one stable IP for everything that context loads.
  • A refused login throws no error: goto() returns a response with status 407. Check response.status(). A gateway 502 throws net::ERR_TUNNEL_CONNECTION_FAILED; a missing authenticate() throws net::ERR_INVALID_AUTH_CREDENTIALS.
  • Use the standard gateways (gw.ludaxproxy.com:7777, Premium gw.ludaxproxy.com:8080). Chrome already sends a real browser TLS handshake, so it does not need the TLS ports.

Using an AI assistant? Give it this guide

One Markdown file with everything ChatGPT, Claude, Cursor or Copilot needs to write working Ludax code for Puppeteer: the connection format, the gateways in force, sessions, errors and tested examples. Copy it into the chat, or paste this prompt:

Read https://ludaxproxy.com/docs/puppeteer.md and use Ludax proxies in my Puppeteer scraper.

[ 01 / HOW THE LOGIN WORKS ]

Why --proxy-server alone is not enough

Chrome takes the proxy’s address from a flag but asks for its login separately, the way it would ask a person. Puppeteer has to answer that question; everything on this page builds on it.

  1. Chrome connects to gw.ludaxproxy.com:7777 without a login, as the flag says.
  2. The gateway answers 407: who are you?
  3. page.authenticate() answers with the username and password, through Chrome’s DevTools protocol, before the page is shown.
WHAT HAPPENS, TESTED WITH PUPPETEER 25.16
You doPuppeteer gives you
Flag plus page.authenticate() with the right loginThe page, status 200
The login inside the flag: --proxy-server=http://user:pass@host:portnet::ERR_NO_SUPPORTED_PROXIES
The flag, no authenticate()net::ERR_INVALID_AUTH_CREDENTIALS
authenticate() with a wrong passwordNo exception: goto() returns status 407 and Chrome’s error page

The last row is the one that costs time: a script that does not check response.status() carries on scraping an error page. Recipe 04 checks it.

[ 02 / FIRST RUN ]

Two lines that log Puppeteer in to a proxy

If the page prints status 200 and a JSON answer with a residential IP, your Puppeteer proxy works.

  1. Have your login ready

    page.authenticate() needs two strings, both on the dashboard’s Generate page. The username is lx_k3m9q2p7xa with resi- in front for Residential, premi- for Residential Premium. The password is the plan’s own; the options go after it (the full list).

  2. Install Puppeteer

    install.sh
    npm install puppeteer

    The puppeteer package downloads its own Chrome. With puppeteer-core pass executablePath to your installed Chrome instead.

  3. Run this

    first_request.mjs
    import puppeteer from "puppeteer";
    
    const browser = await puppeteer.launch({
      args: ["--proxy-server=http://gw.ludaxproxy.com:7777"],
    });
    const page = await browser.newPage();
    // the proxy login: Chrome's flag takes no username or password
    await page.authenticate({ username: "resi-lx_k3m9q2p7xa", password: "YOUR_PASSWORD-country-us" });
    
    const response = await page.goto("https://api.ludaxproxy.com/whoami");
    console.log(response.status(), await page.$eval("body", (body) => body.innerText));
    await browser.close();

    You should see something like this, with your own exit IP:

    output
    200 {"ip": "203.0.113.24", "country": "United States", "type": "residential", "tls_profile": "Chrome 154", "latency_ms": 418}

    That is a real home connection in the United States, picked by -country-us in the password. tls_profile shows the browser your handshake matches: Chrome sends its own, so it needs nothing from the gateway. Change us to de, jp or any code from the locations directory.

    Official docs: page.authenticate() · browser.createBrowserContext()

[ 03 / CODE GENERATOR ]

Puppeteer launch code for your plan

Choose the product, the country and whether the IP should stay the same, and copy the code. Type your own username and password to get code you can run as is: they stay in this browser tab and are never sent anywhere.

Product

Real home IPs for everyday scraping. Gateway: gw.ludaxproxy.com:7777

IP address

YOUR LOGIN

http://resi-lx_k3m9q2p7xa:YOUR_PASSWORD-country-us@gw.ludaxproxy.com:7777
  • username
  • plan password
  • session
  • location
  • gateway
ludax.mjs
[ 04 / RECIPES ]

Puppeteer patterns: contexts, geo, bandwidth, retries

Each one runs on its own: paste it, put in your username and password, run.

01

One IP per browser context: rotating proxies in Puppeteer

Several accounts or jobs in one browser, each from its own IP. A new context with a new session token gives a new IP; the same token keeps it.

contexts.mjs
import puppeteer from "puppeteer";
import { randomBytes } from "node:crypto";

const browser = await puppeteer.launch();
for (const job of ["account-1", "account-2", "account-3"]) {
  // each context has its own proxy and session token: its own IP, held for 30 minutes
  const context = await browser.createBrowserContext({
    proxyServer: "http://gw.ludaxproxy.com:7777",
  });
  const token = randomBytes(4).toString("hex");
  const page = await context.newPage();
  await page.authenticate({
    username: "resi-lx_k3m9q2p7xa",
    password: `YOUR_PASSWORD-session-${token}-time-30-country-us`,
  });
  await page.goto("https://api.ludaxproxy.com/whoami");
  console.log(job, JSON.parse(await page.$eval("body", (body) => body.innerText)).ip);
  await context.close();
}
await browser.close();
output (your IPs will differ)
account-1 203.0.113.24
account-2 198.51.100.7
account-3 192.0.2.66

Why a token per context, and not simply no session? A page is dozens of connections: the document, scripts, images, API calls. Without a token each new connection can leave from a different IP, which no real visitor does. With one token per context everything that context loads shares one IP. Sessions hold up to 1000 minutes; always send -time-.

02

Geo-targeting with a matching language and time zone

Local prices, search results and ads. A German IP with an American browser clock is a mismatch some sites notice: set the language and time zone to the exit’s.

geo.mjs
import puppeteer from "puppeteer";

// an IP in Munich, and a browser that looks local: German language, Berlin time
const browser = await puppeteer.launch({
  args: ["--proxy-server=http://gw.ludaxproxy.com:7777", "--lang=de-DE"],
});
const page = await browser.newPage();
await page.authenticate({
  username: "resi-lx_k3m9q2p7xa",
  password: "YOUR_PASSWORD-session-muc01-time-30-country-de-state-by-city-munich",
});
await page.emulateTimezone("Europe/Berlin");
await page.setExtraHTTPHeaders({ "Accept-Language": "de-DE,de;q=0.9" });

await page.goto("https://api.ludaxproxy.com/whoami");
console.log(await page.$eval("body", (body) => body.innerText));
console.log(await page.evaluate(() => [navigator.language, Intl.DateTimeFormat().resolvedOptions().timeZone]));
await browser.close();
output
{"ip": "203.0.113.81", "country": "Germany", ...}
[ 'de-DE', 'Europe/Berlin' ]

Order matters in the password: session first, then -country-, -state-, -city-. Codes for every country are in the locations directory.

03

Save bandwidth: block images, fonts and media

Plans are priced per GB. Most pages are mostly pictures and fonts that a scraper never reads; abort them in the browser and they never cross the proxy. Request interception and authenticate() work together.

save_data.mjs
import puppeteer from "puppeteer";

const SKIP = new Set(["image", "font", "media"]);

const browser = await puppeteer.launch({
  args: ["--proxy-server=http://gw.ludaxproxy.com:7777"],
});
const page = await browser.newPage();
await page.authenticate({
  username: "resi-lx_k3m9q2p7xa",
  password: "YOUR_PASSWORD-session-wiki01-time-10-country-us",
});
// images, fonts and video never leave the browser, so they never count against your GB
await page.setRequestInterception(true);
page.on("request", (request) =>
  SKIP.has(request.resourceType()) ? request.abort() : request.continue(),
);

await page.goto("https://en.wikipedia.org/wiki/Proxy_server");
console.log(await page.title());
await browser.close();
output
Proxy server - Wikipedia

Official docs: Puppeteer: request interception

04

Handle proxy errors: retry a 502, stop on a refused login

Production scrapers. A dropped exit is worth one more try; a refused login is not, and Puppeteer does not even throw for it.

retry.mjs
import puppeteer from "puppeteer";

// open a URL through Ludax: retry a dropped exit (502), stop at once on a refused login
async function open(page, url, tries = 3) {
  for (let attempt = 1; ; attempt++) {
    try {
      const response = await page.goto(url, { timeout: 30000 });
      // a refused login is not an exception: the page loads with status 407
      if (response?.status() === 407) throw new Error("407: check the username and password");
      return response;
    } catch (error) {
      const dropped = error.message.includes("ERR_TUNNEL_CONNECTION_FAILED");
      if (!dropped || attempt === tries) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** (attempt - 1)));
    }
  }
}

const browser = await puppeteer.launch({
  args: ["--proxy-server=http://gw.ludaxproxy.com:7777"],
});
const page = await browser.newPage();
await page.authenticate({ username: "resi-lx_k3m9q2p7xa", password: "YOUR_PASSWORD-country-us" });
await open(page, "https://api.ludaxproxy.com/whoami");
console.log(await page.$eval("body", (body) => body.innerText));
await browser.close();

These are the answers Puppeteer gives for each gateway reply (Chrome): net::ERR_TUNNEL_CONNECTION_FAILED for a 502; a normal response with status() === 407 when the login is refused; net::ERR_INVALID_AUTH_CREDENTIALS when authenticate() was never called; net::ERR_PROXY_CONNECTION_FAILED when the gateway cannot be reached at all.

05

puppeteer-extra and the stealth plugin

Many scrapers run puppeteer-extra. The proxy is set the same way: the flag at launch and authenticate() on the page.

install_extra.sh
npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
stealth.mjs
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";

puppeteer.use(StealthPlugin());

// the proxy works the same way with puppeteer-extra: a flag, then authenticate()
const browser = await puppeteer.launch({
  args: ["--proxy-server=http://gw.ludaxproxy.com:7777"],
});
const page = await browser.newPage();
await page.authenticate({
  username: "resi-lx_k3m9q2p7xa",
  password: "YOUR_PASSWORD-session-st01-time-30-country-us",
});
await page.goto("https://api.ludaxproxy.com/whoami");
console.log(await page.$eval("body", (body) => body.innerText));
await browser.close();
[ 05 / WHICH PLAN? ]

Which proxy plan for Puppeteer?

Residential or Residential Premium, on the standard ports. The TLS ports are for HTTP libraries, not for browsers.

Before a site reads a single header, the client says hello: the TLS handshake. HTTP libraries such as Node.js fetch say it in a way no browser does, and Residential TLS fixes that at the gateway. Puppeteer drives a real Chrome, so the site already sees a real Chrome handshake: the whoami answer shows it as tls_profile. Use gw.ludaxproxy.com:7777 or Premium gw.ludaxproxy.com:8080: they pass HTTPS through untouched, with no certificate to install.

If a site still blocks your headless Chrome, the cause is the browser’s behaviour or the IP’s reputation: try headed mode, slower pacing, sticky sessions or Residential Premium, a screened pool for tough targets.

Start small, scale on the same username

Puppeteer loads whole pages, so block images and fonts (recipe 03) to make each GB go further. 1 GB is enough to test a target properly.

Get 1 GB — $0.99

Only need the HTML, no JavaScript? A plain HTTP request is much lighter than a browser: see the Node.js guide, and the TLS ports if sites block it.

[ 06 / WHEN IT FAILS ]

What Puppeteer shows when the proxy fails

What Puppeteer shows, what it means and the one thing to do. All gateway answers: status codes.

SYMPTOM, CAUSE, FIX
You seeWhyFix
goto() succeeds, but status() is 407 and the page says “This page isn’t working”The login was refused: a typo, a prefix that does not match the port, or a plan with no data left.Copy the credentials again from Generate; check the plan. Do not loop: 30 failed logins in 5 minutes block your IP for 10 minutes. Every 407 cause.
net::ERR_INVALID_AUTH_CREDENTIALSChrome was asked for a login and nothing answered it.Call page.authenticate() on every page before its first goto(), including pages a context opens later.
net::ERR_TUNNEL_CONNECTION_FAILEDThe gateway answered 502: the exit IP dropped or could not reach the site.Retry (recipe 04). With a sticky session, switch to a new token after two failures. Many at once: check the network status.
net::ERR_PROXY_CONNECTION_FAILEDThe gateway could not be reached: a wrong host or port, or a firewall.Check the flag is --proxy-server=http://gw.ludaxproxy.com:7777 (Premium gw.ludaxproxy.com:8080).
ERR_NO_SUPPORTED_PROXIES or the login is ignoredThe username and password were put in --proxy-server; Chrome does not accept them there.Only the host and port go in the flag. The login goes in page.authenticate().
The IP changes between requests of one pageNo session token: every new connection can get a new IP.Add -session-<token>-time-30 to the password, one token per context (recipe 01).
A SOCKS5 proxy refuses the loginChrome does not send a username and password to SOCKS5 proxies.Use the HTTP gateway: http://gw.ludaxproxy.com:7777. It carries HTTPS too.
[ 07 / FAQ ]

Puppeteer proxy questions

Short answers. The full option list is in proxy authentication.

01How do I use a proxy with a username and password in Puppeteer?

Launch Chrome with --proxy-server=http://gw.ludaxproxy.com:7777, then call page.authenticate({ username: "resi-lx_k3m9q2p7xa", password: "YOUR_PASSWORD" }) before the first page.goto(). The flag takes no credentials; authenticate() answers the proxy login.

02How do I rotate proxies in Puppeteer?

Give each browser context its own session token in the password (-session-<token>-time-<minutes>). Create the context with browser.createBrowserContext({ proxyServer: "http://gw.ludaxproxy.com:7777" }) and call page.authenticate() on its pages. A new token means a new IP; the same token keeps it.

03Can each Puppeteer browser context use a different proxy?

Yes. browser.createBrowserContext({ proxyServer }) sets a proxy for that context only, and page.authenticate() sets its login. One browser can run many contexts, each with its own IP (tested with Puppeteer 25.16).

04Why does Puppeteer show status 407 instead of throwing an error?

When the proxy refuses the login, Chrome shows its error page and goto() returns that response with status 407. Check response.status() and fix the credentials; do not retry in a loop.

05Does the proxy work with puppeteer-extra and the stealth plugin?

Yes. Set --proxy-server at launch and call page.authenticate() on the page, exactly as with plain Puppeteer.

06Do I need the Residential TLS ports for Puppeteer?

No. Puppeteer drives a real Chrome, which already sends a real browser TLS handshake. Use the standard gateways: gw.ludaxproxy.com:7777 for Residential, gw.ludaxproxy.com:8080 for Residential Premium.

07Can I try Ludax with Puppeteer before buying more?

Yes, with the smallest plan: 1 GB of Residential costs $0.99, prepaid, with no subscription. There is no free trial, and your username stays the same when you buy more.

[ START NOW ]

Run your first headless Chrome from a home IP

  1. Buy 1 GB of Residential (Premium for tough targets).
  2. Copy the login from Generate in the dashboard.
  3. Put it in authenticate() with the code generator and run.

Stuck? Send us your launch code and the error on Telegram and we debug it with you. Every plan and price: pricing.