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.
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.
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.
Chrome connects to gw.ludaxproxy.com:7777 without a login, as the flag says.
The gateway answers 407: who are you?
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 do
Puppeteer gives you
Flag plus page.authenticate() with the right login
The page, status 200
The login inside the flag: --proxy-server=http://user:pass@host:port
net::ERR_NO_SUPPORTED_PROXIES
The flag, no authenticate()
net::ERR_INVALID_AUTH_CREDENTIALS
authenticate() with a wrong password
No 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.
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).
Install Puppeteer
install.sh
npm install puppeteer
The puppeteer package downloads its own Chrome. With puppeteer-core pass executablePath to your installed Chrome instead.
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.
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.
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 timeconst 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();
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 = newSet(["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 GBawait 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();
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 loginasyncfunctionopen(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 407if (response?.status() === 407) thrownewError("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;
awaitnewPromise((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" });
awaitopen(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.
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.
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.
[ 08 / NEXT ]
More for browser automation
The products behind the code, and guides for the jobs Puppeteer runs.