Playwright proxy setup with authentication, in four languages
Playwright logs in to a proxy on its own: give it the gateway, your username and your password, and every page loads from a real home IP in the country you pick. No extension, no local forwarder, no SDK. The same code works in Node.js, Python, Java and .NET.
Built-in proxy loginusername and password in the proxy option
One IP per contextrun many identities in one browser
Playwright takes a proxy with server, username and password in launch() or in newContext(): server http://gw.ludaxproxy.com:7777, username resi-lx_k3m9q2p7xa, password YOUR_PASSWORD plus options.
A proxy set on a browser context applies to that context only, so one browser can run many contexts, each with its own IP. This works with no proxy at launch (tested with Playwright 1.64 on Windows).
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.
Use the standard gateways (gw.ludaxproxy.com:7777, Premium gw.ludaxproxy.com:8080). A real browser already sends a real browser TLS handshake, so it does not need the TLS ports.
Playwright’s username and password are for HTTP proxies: use the HTTP gateway, not SOCKS5, when the proxy needs a login.
A gateway 502 shows as net::ERR_TUNNEL_CONNECTION_FAILED (retry it); a refused login as a navigation error such as net::ERR_HTTP_RESPONSE_CODE_FAILURE (fix the credentials).
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 Playwright: 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/playwright.md and use Ludax proxies in my Playwright project.
If the page prints a JSON answer with a residential IP, your Playwright proxy works. Pick your language once: every code block on this page follows it.
Get your username and password
In the dashboard, open Generate. Your username looks like lx_k3m9q2p7xa and gets the product prefix: resi- for Residential, premi- for Residential Premium. Each plan has its own 16-character password. The full format is in proxy authentication.
No account yet? Get 1 GB of Residential for $0.99: an email address is all it takes, you pay in crypto, and you are ready in a minute.
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 of the 181 country codes.
Choose the product, the country and whether the IP should stay the same, and copy the code for your language. 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.
Playwright proxy recipes: contexts, rotation, geo and errors
Each one runs on its own: paste it, put in your username and password, run. They follow the language you picked above.
01
One IP per browser context: rotating proxies in Playwright
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 { chromium } from"playwright";
import { randomBytes } from"node:crypto";
const GATEWAY = "http://gw.ludaxproxy.com:7777";
const USER = "resi-lx_k3m9q2p7xa";
const PASS = "YOUR_PASSWORD";
const browser = await chromium.launch();
for (const job of ["account-1", "account-2", "account-3"]) {
// a new session token = a new IP, held by this context for 30 minutesconst token = randomBytes(4).toString("hex");
const context = await browser.newContext({
proxy: { server: GATEWAY, username: USER, password: `${PASS}-session-${token}-time-30-country-us` },
});
const page = await context.newPage();
await page.goto("https://api.ludaxproxy.com/whoami");
console.log(job, JSON.parse(await page.textContent("body")).ip);
await context.close();
}
await browser.close();
contexts.py
import json
import secrets
from playwright.sync_apiimport sync_playwright
GATEWAY = "http://gw.ludaxproxy.com:7777"
USER = "resi-lx_k3m9q2p7xa"
PASS = "YOUR_PASSWORD"withsync_playwright() as p:
browser = p.chromium.launch()
for job in ["account-1", "account-2", "account-3"]:
# a new session token = a new IP, held by this context for 30 minutes
token = secrets.token_hex(4)
context = browser.new_context(proxy={
"server": GATEWAY,
"username": USER,
"password": f"{PASS}-session-{token}-time-30-country-us",
})
page = context.new_page()
page.goto("https://api.ludaxproxy.com/whoami")
print(job, json.loads(page.text_content("body"))["ip"])
context.close()
browser.close()
Contexts.java
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.Proxy;
import java.security.SecureRandom;
import java.util.HexFormat;
publicclass Contexts {
publicstaticvoidmain(String[] args) {
SecureRandom random = newSecureRandom();
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
for (String job : new String[] {"account-1", "account-2", "account-3"}) {
// a new session token = a new IP, held by this context for 30 minutes
byte[] bytes = new byte[4];
random.nextBytes(bytes);
String token = HexFormat.of().formatHex(bytes);
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setProxy(newProxy("http://gw.ludaxproxy.com:7777")
.setUsername("resi-lx_k3m9q2p7xa")
.setPassword("YOUR_PASSWORD-session-" + token + "-time-30-country-us")));
Page page = context.newPage();
page.navigate("https://api.ludaxproxy.com/whoami");
System.out.println(job + " " + page.textContent("body"));
context.close();
}
}
}
}
Contexts.cs
using System.Security.Cryptography;
using Microsoft.Playwright;
usingvar playwright = await Playwright.CreateAsync();
awaitusingvar browser = await playwright.Chromium.LaunchAsync();
foreach (var job innew[] { "account-1", "account-2", "account-3" })
{
// a new session token = a new IP, held by this context for 30 minutesvar token = Convert.ToHexString(RandomNumberGenerator.GetBytes(4)).ToLowerInvariant();
awaitusingvar context = await browser.NewContextAsync(new()
{
Proxy = new Proxy
{
Server = "http://gw.ludaxproxy.com:7777",
Username = "resi-lx_k3m9q2p7xa",
Password = $"YOUR_PASSWORD-session-{token}-time-30-country-us",
},
});
var page = await context.NewPageAsync();
await page.GotoAsync("https://api.ludaxproxy.com/whoami");
Console.WriteLine($"{job} {await page.TextContentAsync("body")}");
}
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, and a new context starts fresh. Sessions hold up to 1000 minutes on Residential and Premium; always send -time-.
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 context’s locale and time zone to the exit’s.
geo.mjs
import { chromium } from"playwright";
const browser = await chromium.launch();
// an IP in Munich, and a browser that looks local: German language, Berlin timeconst context = await browser.newContext({
proxy: {
server: "http://gw.ludaxproxy.com:7777",
username: "resi-lx_k3m9q2p7xa",
password: "YOUR_PASSWORD-session-muc01-time-30-country-de-state-by-city-munich",
},
locale: "de-DE",
timezoneId: "Europe/Berlin",
});
const page = await context.newPage();
await page.goto("https://api.ludaxproxy.com/whoami");
console.log(await page.textContent("body"));
console.log(await page.evaluate(() => [navigator.language, Intl.DateTimeFormat().resolvedOptions().timeZone]));
await browser.close();
geo.py
from playwright.sync_apiimport sync_playwright
withsync_playwright() as p:
browser = p.chromium.launch()
# an IP in Munich, and a browser that looks local: German language, Berlin time
context = browser.new_context(
proxy={
"server": "http://gw.ludaxproxy.com:7777",
"username": "resi-lx_k3m9q2p7xa",
"password": "YOUR_PASSWORD-session-muc01-time-30-country-de-state-by-city-munich",
},
locale="de-DE",
timezone_id="Europe/Berlin",
)
page = context.new_page()
page.goto("https://api.ludaxproxy.com/whoami")
print(page.text_content("body"))
print(page.evaluate("() => [navigator.language, Intl.DateTimeFormat().resolvedOptions().timeZone]"))
browser.close()
Geo.java
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.Proxy;
publicclass Geo {
publicstaticvoidmain(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
// an IP in Munich, and a browser that looks local: German language, Berlin time
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setProxy(newProxy("http://gw.ludaxproxy.com:7777")
.setUsername("resi-lx_k3m9q2p7xa")
.setPassword("YOUR_PASSWORD-session-muc01-time-30-country-de-state-by-city-munich"))
.setLocale("de-DE")
.setTimezoneId("Europe/Berlin"));
Page page = context.newPage();
page.navigate("https://api.ludaxproxy.com/whoami");
System.out.println(page.textContent("body"));
System.out.println(page.evaluate("() => [navigator.language, Intl.DateTimeFormat().resolvedOptions().timeZone]"));
}
}
}
Geo.cs
using Microsoft.Playwright;
usingvar playwright = await Playwright.CreateAsync();
awaitusingvar browser = await playwright.Chromium.LaunchAsync();
// an IP in Munich, and a browser that looks local: German language, Berlin timevar context = await browser.NewContextAsync(new()
{
Proxy = new Proxy
{
Server = "http://gw.ludaxproxy.com:7777",
Username = "resi-lx_k3m9q2p7xa",
Password = "YOUR_PASSWORD-session-muc01-time-30-country-de-state-by-city-munich",
},
Locale = "de-DE",
TimezoneId = "Europe/Berlin",
});
var page = await context.NewPageAsync();
await page.GotoAsync("https://api.ludaxproxy.com/whoami");
Console.WriteLine(await page.TextContentAsync("body"));
Console.WriteLine(await page.EvaluateAsync<string>("() => navigator.language + ' ' + Intl.DateTimeFormat().resolvedOptions().timeZone"));
Order matters in the password: session first, then -country-, -state-, -city-. Region and city codes for every country are in the locations directory, for example Germany and the United States.
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.
save_data.mjs
import { chromium } from"playwright";
const SKIP = newSet(["image", "font", "media"]);
const browser = await chromium.launch({
proxy: {
server: "http://gw.ludaxproxy.com:7777",
username: "resi-lx_k3m9q2p7xa",
password: "YOUR_PASSWORD-session-wiki01-time-10-country-us",
},
});
const context = await browser.newContext();
// images, fonts and video never leave the browser, so they never count against your GBawait context.route("**/*", (route) =>
SKIP.has(route.request().resourceType()) ? route.abort() : route.continue(),
);
const page = await context.newPage();
await page.goto("https://en.wikipedia.org/wiki/Proxy_server");
console.log(await page.title());
await browser.close();
save_data.py
from playwright.sync_apiimport sync_playwright
SKIP = {"image", "font", "media"}
withsync_playwright() as p:
browser = p.chromium.launch(proxy={
"server": "http://gw.ludaxproxy.com:7777",
"username": "resi-lx_k3m9q2p7xa",
"password": "YOUR_PASSWORD-session-wiki01-time-10-country-us",
})
context = browser.new_context()
# images, fonts and video never leave the browser, so they never count against your GB
context.route("**/*", lambda route: route.abort()
if route.request.resource_typein SKIP else route.continue_())
page = context.new_page()
page.goto("https://en.wikipedia.org/wiki/Proxy_server")
print(page.title())
browser.close()
SaveData.java
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.Proxy;
import java.util.Set;
publicclass SaveData {
publicstaticvoidmain(String[] args) {
Set<String> skip = Set.of("image", "font", "media");
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setProxy(newProxy("http://gw.ludaxproxy.com:7777")
.setUsername("resi-lx_k3m9q2p7xa")
.setPassword("YOUR_PASSWORD-session-wiki01-time-10-country-us")));
BrowserContext context = browser.newContext();
// images, fonts and video never leave the browser, so they never count against your GB
context.route("**/*", route -> {
if (skip.contains(route.request().resourceType())) route.abort();
else route.resume();
});
Page page = context.newPage();
page.navigate("https://en.wikipedia.org/wiki/Proxy_server");
System.out.println(page.title());
}
}
}
SaveData.cs
using Microsoft.Playwright;
var skip = new HashSet<string> { "image", "font", "media" };
usingvar playwright = await Playwright.CreateAsync();
awaitusingvar browser = await playwright.Chromium.LaunchAsync(new()
{
Proxy = new Proxy
{
Server = "http://gw.ludaxproxy.com:7777",
Username = "resi-lx_k3m9q2p7xa",
Password = "YOUR_PASSWORD-session-wiki01-time-10-country-us",
},
});
var context = await browser.NewContextAsync();
// images, fonts and video never leave the browser, so they never count against your GBawait context.RouteAsync("**/*", async route =>
{
if (skip.Contains(route.Request.ResourceType)) await route.AbortAsync();
elseawait route.ContinueAsync();
});
var page = await context.NewPageAsync();
await page.GotoAsync("https://en.wikipedia.org/wiki/Proxy_server");
Console.WriteLine(await page.TitleAsync());
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.
retry.mjs
import { chromium } from"playwright";
// Open a URL through Ludax: retry a dropped exit (502), stop at once on anything else.asyncfunctionopen(page, url, tries = 3) {
for (let attempt = 1; ; attempt++) {
try {
returnawait page.goto(url, { timeout: 30000 });
} catch (error) {
const dropped = error.message.includes("ERR_TUNNEL_CONNECTION_FAILED");
if (!dropped || attempt === tries) throw error; // a refused login will not fix itselfawaitnewPromise((resolve) => setTimeout(resolve, 1000 * 2 ** (attempt - 1)));
}
}
}
const browser = await chromium.launch({
proxy: {
server: "http://gw.ludaxproxy.com:7777",
username: "resi-lx_k3m9q2p7xa",
password: "YOUR_PASSWORD-country-us",
},
});
const page = await browser.newPage();
awaitopen(page, "https://api.ludaxproxy.com/whoami");
console.log(await page.textContent("body"));
await browser.close();
retry.py
import time
from playwright.sync_apiimport sync_playwright, Error
defopen_page(page, url, tries=3):
"""Open a URL through Ludax: retry a dropped exit (502), stop at once on anything else."""for attempt inrange(1, tries + 1):
try:
return page.goto(url, timeout=30000)
except Error as error:
dropped = "ERR_TUNNEL_CONNECTION_FAILED"in error.messageifnot dropped or attempt == tries:
raise# a refused login will not fix itself
time.sleep(2 ** (attempt - 1))
withsync_playwright() as p:
browser = p.chromium.launch(proxy={
"server": "http://gw.ludaxproxy.com:7777",
"username": "resi-lx_k3m9q2p7xa",
"password": "YOUR_PASSWORD-country-us",
})
page = browser.new_page()
open_page(page, "https://api.ludaxproxy.com/whoami")
print(page.text_content("body"))
browser.close()
Retry.java
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.Proxy;
publicclass Retry {
// Open a URL through Ludax: retry a dropped exit (502), stop at once on anything else.static Response open(Page page, String url, int tries) throws InterruptedException {
for (int attempt = 1; ; attempt++) {
try {
return page.navigate(url);
} catch (PlaywrightException error) {
boolean dropped = error.getMessage().contains("ERR_TUNNEL_CONNECTION_FAILED");
if (!dropped || attempt == tries) throw error; // a refused login will not fix itself
Thread.sleep(1000L << (attempt - 1));
}
}
}
publicstaticvoidmain(String[] args) throws InterruptedException {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setProxy(newProxy("http://gw.ludaxproxy.com:7777")
.setUsername("resi-lx_k3m9q2p7xa")
.setPassword("YOUR_PASSWORD-country-us")));
Page page = browser.newPage();
open(page, "https://api.ludaxproxy.com/whoami", 3);
System.out.println(page.textContent("body"));
}
}
}
Retry.cs
using Microsoft.Playwright;
usingvar playwright = await Playwright.CreateAsync();
awaitusingvar browser = await playwright.Chromium.LaunchAsync(new()
{
Proxy = new Proxy
{
Server = "http://gw.ludaxproxy.com:7777",
Username = "resi-lx_k3m9q2p7xa",
Password = "YOUR_PASSWORD-country-us",
},
});
var page = await browser.NewPageAsync();
awaitOpen(page, "https://api.ludaxproxy.com/whoami");
Console.WriteLine(await page.TextContentAsync("body"));
// Open a URL through Ludax: retry a dropped exit (502), stop at once on anything else.staticasync Task<IResponse?> Open(IPage page, string url, int tries = 3)
{
for (var attempt = 1; ; attempt++)
{
try
{
returnawait page.GotoAsync(url);
}
catch (PlaywrightException error) when (
error.Message.Contains("ERR_TUNNEL_CONNECTION_FAILED") && attempt < tries)
{
await Task.Delay(1000 * (1 << (attempt - 1))); // a refused login is not caught: it will not fix itself
}
}
}
These are the messages Playwright gives for each gateway answer (Chrome): net::ERR_TUNNEL_CONNECTION_FAILED for a 502, net::ERR_HTTP_RESPONSE_CODE_FAILURE when the login is refused, net::ERR_PROXY_CONNECTION_FAILED when the gateway cannot be reached at all.
05
Playwright Test: a proxy for the whole test run
End-to-end tests that must see a site from another country. Put the proxy in playwright.config.ts and every test uses it; keep the password in an environment variable.
playwright.config.ts
import { defineConfig } from"@playwright/test";
export default defineConfig({
use: {
// every test in the run uses one US IP for 60 minutesproxy: {
server: "http://gw.ludaxproxy.com:7777",
username: "resi-lx_k3m9q2p7xa",
password: `${process.env.LUDAX_PASSWORD}-session-ci01-time-60-country-us`,
},
},
});
No. The TLS ports exist for HTTP libraries, which give themselves away in the TLS handshake. A real browser does not.
Before a site reads a single header, the client says hello: the TLS handshake. Python’s requests or Node.js fetch say it in a way no browser does, and anti-bot systems block on that. Residential TLS fixes it at the gateway, by connecting with a real browser’s handshake.
Playwright drives a real Chromium, so the site already sees a real Chrome handshake: the whoami answer above shows it as tls_profile. Use the standard gateways, 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 a headless browser, the cause is the browser’s behaviour or the IP’s reputation, not the handshake: try headed mode, slower pacing, sticky sessions or Residential Premium.
[ 05 / TROUBLESHOOTING ]
Playwright proxy errors and fixes
What Playwright shows, what it means and the one thing to do. All gateway answers: status codes.
SYMPTOM, CAUSE, FIX
You see
Why
Fix
net::ERR_TUNNEL_CONNECTION_FAILED
The 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_HTTP_RESPONSE_CODE_FAILURE or a 407
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_PROXY_CONNECTION_FAILED
The gateway could not be reached: a wrong host or port, or a firewall.
Check the server is http://gw.ludaxproxy.com:7777 (Premium gw.ludaxproxy.com:8080) and that your network allows outgoing connections to it.
The IP changes between requests of one page
No session token: every new connection can get a new IP.
Add -session-<token>-time-30 to the password, one token per context (recipe 01).
The proxy works for one context only
All contexts share the launch proxy and one token.
Set the proxy in newContext(), with a different token for each context.
A SOCKS5 proxy refuses the login
Playwright passes a username and password to HTTP proxies.
Use the HTTP gateway: http://gw.ludaxproxy.com:7777.
A different city than asked
No free exit in that city at that moment: the request falls back to the country.
Check where you landed with whoami; the locations directory lists the cities of every country.
01How do I use a proxy with a username and password in Playwright?
Pass them in the proxy option: server http://gw.ludaxproxy.com:7777, username resi-lx_k3m9q2p7xa and password YOUR_PASSWORD with your options. It works in launch() for the whole browser or in newContext() for one context, in Node.js, Python, Java and .NET. There is nothing else to install.
02How do I rotate proxies in Playwright?
Give each browser context its own session token in the password (-session-<token>-time-<minutes>). A new token means a new IP; the same token keeps it. Without any token each new connection can leave from a new IP, even within one page.
03Can each Playwright context have a different proxy?
Yes. Set the proxy in browser.newContext() and it applies to that context only. One browser can run many contexts, each with its own IP. It works without a proxy at launch (tested with Playwright 1.64 on Windows).
04Do I need the Residential TLS ports for Playwright?
No. Playwright drives a real browser, 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.
05Does Playwright support SOCKS5 proxies with authentication?
Playwright’s username and password apply to HTTP proxies. Use the Ludax HTTP gateway, which carries HTTPS too, whenever the proxy needs a login.
06Is there a free trial?
No: the smallest plan is the test. 1 GB of Residential costs $0.99, with no subscription, and your username stays the same when you buy more.
[ 07 / KEEP GOING ]
Where to go from here
The products behind the code, and guides for the jobs Playwright runs.