Integration · Browser automation
How to use a proxy with Playwright
Playwright takes a proxy as an object with server, username and password, either on launch() for the whole browser or on new_context() for one isolated context. Point server at the ARC gateway, put the pool, country and session in the username, and every context can leave from its own country or hold its own sticky session.
Reviewed 30 Oct 2026
http://USERNAME _pool-arcfast _country-de:PASSWORD@ proxy.arcproxies.io:8000The short answer
- Credentials go in
usernameandpassword, never inside theserverURL. - Use one browser context per task, each with its own
_session-handle: contexts are cheap, and each one gets its own exit IP and cookie jar. - In a browser, sticky beats rotating: one page load opens many connections, and a rotating username can spread them over several exits.
- Check the exit IP once before a long run by loading an IP echo service through the same proxy settings.
Setup
Set up Playwright with a proxy, step by step
- 01
Install Playwright and a browser
Both the Python and the Node packages ship the same
proxyoption. Install the library, then the browser build it drives.shell7 lines# Pythonpip install playwrightplaywright install chromium# Nodenpm i playwrightnpx playwright install chromium - 02
Launch through the gateway and check the exit IP (Python)
The
servertakes scheme, host and port; the username carries the targeting. Loading an IP echo service shows the address the target will see, which should be a German residential IP for this username, not your own.check_exit.py12 linesfrom playwright.sync_api import sync_playwrightwith sync_playwright() as p: browser = p.chromium.launch(proxy={ "server": "http://proxy.arcproxies.io:8000", "username": "USERNAME_pool-arcfast_country-de_session-k3f9a1x2_sesstime-10", "password": "PASSWORD", }) page = browser.new_page() page.goto("https://api.ipify.org?format=json") print(page.text_content("body")) # {"ip":"…"}: the exit, not your machine browser.close() - 03
The same in Node
Identical option names in JavaScript and TypeScript. The test runner takes the same object under
use.proxyinplaywright.config.ts.check-exit.mjs13 linesimport { chromium } from 'playwright';const browser = await chromium.launch({ proxy: { server: 'http://proxy.arcproxies.io:8000', username: 'USERNAME_pool-arcfast_country-de_session-k3f9a1x2_sesstime-10', password: 'PASSWORD', },});const page = await browser.newPage();await page.goto('https://api.ipify.org?format=json');console.log(await page.textContent('body'));await browser.close(); - 04
One context per task, one session handle per context
For concurrent work, launch the browser once without a proxy and give each
new_context()its own proxy object. A fresh_session-handle means a fresh exit IP that then stays put for the task;_sesstime-10holds it for up to ten minutes. A semaphore caps how many contexts are open at once.contexts.py36 linesimport asyncioimport secretsfrom playwright.async_api import async_playwrightSERVER = "http://proxy.arcproxies.io:8000"BASE = "USERNAME_pool-arcfast_country-de" # pool + country; the session is added per taskasync def run_task(browser, url): handle = secrets.token_hex(4) # new handle, new exit IP context = await browser.new_context(proxy={ "server": SERVER, "username": f"{BASE}_session-{handle}_sesstime-10", "password": "PASSWORD", }) try: page = await context.new_page() await page.goto(url, wait_until="domcontentloaded") return await page.title() finally: await context.close()async def main(urls): async with async_playwright() as p: browser = await p.chromium.launch() limit = asyncio.Semaphore(5) # contexts open at once async def guarded(url): async with limit: return await run_task(browser, url) print(await asyncio.gather(*(guarded(u) for u in urls))) await browser.close()asyncio.run(main(["https://example.com/a", "https://example.com/b"])) - 05
Target a country or city per context
Geo-targeting is the username, so a context per market is a different username per context.
_country-takes a lowercase ISO code and_city-a city from the published list, lowercased with spaces removed. Settinglocaleandtimezone_idto match makes the page render the way a local visitor sees it.markets.py17 linesMARKETS = { # label: (username, locale, timezone) "de-berlin": ("USERNAME_pool-arcfast_country-de_city-berlin", "de-DE", "Europe/Berlin"), "fr-paris": ("USERNAME_pool-arcfast_country-fr_city-paris", "fr-FR", "Europe/Paris"), "us-newyork": ("USERNAME_pool-arcfast_country-us_city-newyork", "en-US", "America/New_York"),}for label, (user, locale, tz) in MARKETS.items(): context = browser.new_context( proxy={"server": "http://proxy.arcproxies.io:8000", "username": user, "password": "PASSWORD"}, locale=locale, timezone_id=tz, ) page = context.new_page() page.goto("https://example.com/product/123") print(label, page.locator("[data-price]").first.text_content()) context.close() - 06
Handle proxy errors and retry on a new exit
Proxy failures do not come back as a response:
goto()raises anet::ERR_…error or a timeout. Rate limits do come back as a response. Retry both on a fresh session handle with backoff, and stop after a few attempts. A wrong password fails every attempt the same way, so run the exit-IP check above first.retry.py27 linesimport secretsimport timefrom playwright.sync_api import Error as PlaywrightErrorBASE = "USERNAME_pool-arcfast_country-de"def fetch_html(browser, url, attempts=3): for attempt in range(attempts): context = browser.new_context(proxy={ "server": "http://proxy.arcproxies.io:8000", "username": f"{BASE}_session-{secrets.token_hex(4)}_sesstime-10", "password": "PASSWORD", }) try: page = context.new_page() response = page.goto(url, timeout=30_000) if response and (response.status == 429 or response.status >= 500): time.sleep(5 * 2 ** attempt) # back off, then a new exit continue return page.content() except PlaywrightError as err: # TimeoutError is a subclass print(f"attempt {attempt + 1}: {err.message.splitlines()[0]}") time.sleep(2 ** attempt) finally: context.close() raise RuntimeError(f"gave up on {url}")
Launch-level or context-level proxy in Playwright?
Both take the same object; the difference is scope. Pick by how many identities one browser process has to carry.
| Behaviour | launch(proxy=…) | new_context(proxy=…) |
|---|---|---|
| Scope | Every page in the browser | Pages in that context only |
| Identities per browser | One | One per context, as many as you open |
| Typical use | A single agent or script, one task at a time | Parallel tasks, one market or session each |
| Changing exit mid-run | Relaunch the browser | Close the context, open a new one with a new handle |
Contexts do not share cookies or storage, so a context per sticky session keeps the cookie jar and the IP aligned: the site sees one visitor at one address for the length of the task.
Rotating or sticky sessions in a headless browser
A rotating username (no _session- token) lets each new connection leave from a different exit. That suits a plain HTTP client fetching independent URLs. A headless browser is different: one page load opens connections for the document, scripts, images and API calls, and a rotating username can spread those across several IPs inside one visit.
So in Playwright, default to sticky: one handle per context, _sesstime- long enough to finish the task, and a new handle for the next task. Rotating vs sticky sessions covers the trade-off in more depth. Sticky holds are best effort: an exit can drop before the time is up, so treat a changed IP as something to retry, not as a bug.
SOCKS5, HTTPS targets and CONNECT
- Use `http://` for the server. The gateway speaks HTTP and SOCKS5 on the same port with the same credentials, but Playwright documents
usernameandpasswordfor HTTP proxies only.http://is the scheme that is documented to authenticate. - HTTPS sites still work over an `http://` proxy. The browser opens an HTTP CONNECT tunnel and runs TLS end to end with the site; the gateway sees the hostname, not the content.
- `bypass` takes a comma-separated list of hosts that should not go through the proxy, such as an internal API the page also calls.
What a browser costs in bandwidth
A rendered page usually transfers far more than its HTML: scripts, fonts, images and third-party tags all count against a per-GB balance. Blocking images, media and fonts with context.route() is the largest single saving, but routing turns off Playwright's HTTP cache, so measure a multi-page walk both ways. Reducing proxy bandwidth has the full checklist, and Crawlee adds a crawl queue and session pool on top of Playwright if you need one.
The problem
Common pitfalls
Credentials inside the server URL
server is scheme, host and port. Put the username and password in their own fields; credentials pasted into the URL are not how the option is documented to work.
A rotating username for a multi-step task
Logged-out flows like search, filter, paginate can break when the IP changes between steps. Add _session-…_sesstime-… and keep the handle for the whole task.
Sharing one handle across parallel contexts
Two contexts with the same handle share one exit IP, which concentrates their traffic on one address and its rate limit. Mint a handle per context.
Background requests from branded Chrome
With a branded Chrome (channel="chrome"), the browser's own update and account checks can also go through the proxy and count as traffic. In our tests they appeared within seconds of launch.
FAQ
Questions
Something not covered? Write to contact@arcproxies.io.
How do I add proxy authentication in Playwright?
Pass proxy={"server": "http://host:port", "username": "…", "password": "…"} to chromium.launch() or to browser.new_context(). Playwright answers the proxy's authentication challenge itself, so no extension or extra code is needed.
Can each Playwright context use a different proxy?
Yes. browser.new_context(proxy={...}) sets the proxy for that context only. With ARC, give each context a different _session- handle in the username and each one gets its own exit IP, or a different _country- token and each one leaves from a different country.
How do I check which IP Playwright is using?
Open an IP echo service such as https://api.ipify.org?format=json in a page that uses the same proxy settings and read the body. With a sticky username, the address stays the same for the length of _sesstime-, so the check is valid for the task that follows.
Does Playwright work with a SOCKS5 proxy?
Playwright accepts socks5:// servers, but documents username and password authentication for HTTP proxies only. Because the ARC gateway serves HTTP and SOCKS5 on the same port with the same credentials, use http:// in Playwright and nothing is lost.
Why does my Playwright script fail with net::ERR errors through a proxy?
Proxy-level failures, such as rejected credentials or a dropped tunnel, surface as net::ERR_… exceptions from goto() rather than as an HTTP status. Check the credentials once with an IP echo page, then retry other failures on a new session handle with backoff.
Sources & review
Last reviewed by the ARC Proxies team. Prices are read from the live catalog at build time.
- [1]Playwright: Network: HTTP proxy
- [2]Playwright: BrowserType.launch: proxy option (Python)
- [3]Playwright: Browser.new_context: proxy option (Python)
Next
What people build with it
Integrations
Other browser automation
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.