How to use a proxy with Playwright (Python and Node.js)
Pass proxy={"server": "http://GATEWAY_IP:PORT"} to chromium.launch() to send the whole browser through one gateway, or pass it to browser.new_context() to give each context its own gateway. Playwright handles HTTPS tunnelling itself. Plan for about 10 proxy threads per open tab.
Playwright is Microsoft’s browser automation library. It drives Chromium, Firefox and WebKit from Python, Node.js, Java or .NET, and it’s the usual choice for pages that only show their content after JavaScript runs. Unlike Puppeteer and Selenium, it treats proxies as a first-class option with fields for the server, a bypass list and credentials, at both the browser and the context level.
People route Playwright through proxies to spread a crawl across many IPs, to see a site as a US or EU visitor, and to keep separate accounts in separate contexts with separate IPs. What goes wrong is rarely the syntax. It’s that a real browser opens many connections per page, so a plan sized for a script runs out of threads; that a context reuses its connections and keeps one exit IP longer than expected; and that a 30-second navigation timeout is tight once a heavy page goes through a proxy.
This guide shows the setup in Python and Node.js side by side, explains when to set the proxy at launch and when per context, works out the thread math for parallel tabs, and lists the errors you’ll see with their fixes.
Which Storm plan fits
For Playwright scraping, take a rotating proxy plan with room for tabs: each open page uses around 10 threads, so 40 threads runs about 4 pages at once and 150 threads about 15. The 10-thread package is only for a first test. For logged-in sessions that must keep one IP, use the 15-minute gateway, a residential port per context, or a private dedicated proxy per account.
Rotating proxies (from $14/mo): 700,000+ IPs behind fixed gateway IP:PORTs. New IP on every request, or every 3 or 15 minutes. USA, EU, USA+EU or Worldwide. Unlimited bandwidth on every plan.
Get 40 threads for $39/mo See all rotating proxies plansBefore you start
- Log in to the member area and copy your gateway
IP:PORTs. They never change; the rotation happens on our side. - Add the public IP of the computer or server that will run your tool under Authorized IPs, click Save, and allow up to 15 minutes before testing. Rotating and residential proxies use IP authentication, so there is no username or password.
- Dedicated proxies work with either IP authentication or a username and password. Use user:pass if your IP changes or the tool runs on several machines.
- Count your threads: the tool’s total open connections must stay within your plan (for example 40 threads on the 40-thread plan).
- Set up a Playwright proxy, step by step
- Playwright proxy examples (Python and Node.js)
- Launch-level or context-level: which one to use
- Thread math for tabs and contexts
- Rotation inside a browser: what to expect
- Verify the proxy the right way
- Running Playwright on servers and in CI
- Common errors and fixes
- FAQ
Set up a Playwright proxy, step by step
- Install Playwright and a browser
Python:
pip install playwright, thenplaywright install chromium. Node.js:npm i playwright(ornpm init playwright@latestfor the test runner), thennpx playwright install chromium. - Decide: one proxy for the browser, or one per context
One gateway for everything (typical for scraping with the Main rotating gateway): set it in
launch(). Different gateways for different jobs or accounts: set it innew_context(). You can combine them; a context’s own proxy overrides the launch one. - Write the proxy as a dict
The option is an object, not a URL string:
{"server": "http://GATEWAY_IP:PORT"}. Addusernameandpasswordkeys only for dedicated proxies that use user:pass. Addbypassfor hosts that should go direct, as a comma-separated string. - Open a page and check the IP
Go to
https://httpbin.org/ipand print the body. A pool IP means the proxy works. Do this once per context when each one has its own gateway, so you know every context left from where you expect. - Size your parallel pages
Count around 10 threads per open page. Keep open pages (tabs across all contexts and browsers) at your plan’s threads divided by 10. Close pages as soon as you’ve read them; an idle tab can still hold connections.
- Set timeouts for proxied loading
Raise the default navigation timeout from 30 to around 60 seconds with
context.set_default_navigation_timeout(60_000), and usewait_until="domcontentloaded"when you don’t need every image and tracker to finish. - Run headless in production
Chromium launches headless by default. Use
headless=Falsewhile you debug and watch where the page stalls, then switch back for the real run.
Playwright proxy examples (Python and Node.js)
Replace GATEWAY_IP:PORT with gateways from your member area. Rotating and residential gateways recognise your authorized IP, so they need no username or password in Playwright.
from playwright.sync_api import sync_playwright
GATEWAY = "http://GATEWAY_IP:PORT"
with sync_playwright() as p:
browser = p.chromium.launch(proxy={"server": GATEWAY, "bypass": "localhost,127.0.0.1"})
page = browser.new_page()
page.goto("https://httpbin.org/ip", timeout=60_000)
print(page.inner_text("body"))
browser.close()const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({
proxy: { server: 'http://GATEWAY_IP:PORT' },
});
const page = await browser.newPage();
await page.goto('https://httpbin.org/ip', { timeout: 60000 });
console.log(await page.textContent('body'));
await browser.close();
})();import asyncio
from playwright.async_api import async_playwright
GATEWAYS = { # e.g. three residential ports = three IPs at once
"us-1": "http://GATEWAY_IP:PORT1",
"us-2": "http://GATEWAY_IP:PORT2",
"eu-1": "http://GATEWAY_IP:PORT3",
}
async def run(browser, name, server):
ctx = await browser.new_context(proxy={"server": server}, locale="en-US")
ctx.set_default_navigation_timeout(60_000)
page = await ctx.new_page()
await page.goto("https://httpbin.org/ip", wait_until="domcontentloaded")
print(name, await page.inner_text("body"))
await ctx.close()
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
await asyncio.gather(*(run(browser, n, s) for n, s in GATEWAYS.items()))
await browser.close()
asyncio.run(main())const { chromium } = require('playwright');
const PLAN_THREADS = 40;
const MAX_PAGES = Math.floor(PLAN_THREADS / 10); // about 10 threads per page
(async () => {
const browser = await chromium.launch({ proxy: { server: 'http://GATEWAY_IP:PORT' } });
const urls = Array.from({ length: 20 }, (_, i) => `https://httpbin.org/anything/${i}`);
async function worker() {
const ctx = await browser.newContext();
ctx.setDefaultNavigationTimeout(60000);
while (urls.length) {
const url = urls.shift();
const page = await ctx.newPage();
try {
const res = await page.goto(url, { waitUntil: 'domcontentloaded' });
console.log(url, res && res.status());
} catch (e) {
console.log(url, e.message);
} finally {
await page.close(); // frees its connections
}
}
await ctx.close();
}
await Promise.all(Array.from({ length: MAX_PAGES }, worker));
await browser.close();
})();# Python
context = browser.new_context(proxy={
"server": "http://PROXY_IP:PORT",
"username": "USERNAME",
"password": "PASSWORD", # plain text here, no URL-encoding needed
})
// Node.js
const context = await browser.newContext({
proxy: { server: 'http://PROXY_IP:PORT', username: 'USERNAME', password: 'PASSWORD' },
});import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
proxy: { server: 'http://GATEWAY_IP:PORT' },
navigationTimeout: 60_000,
},
workers: 4, // 4 workers x ~10 threads = a 40-thread plan
});Launch-level or context-level: which one to use
A launch-level proxy applies to every context and page that browser opens. It’s the simplest choice when one gateway serves the whole job, for example the Main rotating gateway for a product crawl. It also keeps the code honest: nothing can leak out through your own IP because a context forgot its proxy.
A context-level proxy gives each BrowserContext its own server. A context is a separate, cookie-isolated session inside one browser process, much cheaper than launching a new browser. Use this when contexts play different roles:
- One context per account, each on its own residential port or dedicated proxy, so accounts never share an IP.
- US pages through a USA gateway and EU pages through an EU gateway in the same run.
- A login flow on the 15-minute gateway beside a crawl on the Main one.
Combining both is a good default: a launch proxy as the fallback, so a context created without one still goes through a gateway, and context proxies wherever a job needs its own IP.
Thread math for tabs and contexts
A script with requests uses one connection per request. A browser page doesn’t: the HTML, scripts, stylesheets, fonts, images and background API calls load in parallel, and Chromium opens several connections per host. Plan for about 10 threads per open page.
- 10-thread package: 1 page. Fine to test the setup, not to scrape.
- 40 threads: 4 pages in parallel. 80: 8. 150: 15. 200: 20.
- Residential: each port allows up to 50 threads on one IP, so one port carries around 5 pages that all share its current IP. For 5 pages on 5 different IPs, use 5 ports, one per context.
- Dedicated: 100 threads per proxy, around 10 pages, always from the same static IP.
Blocking what you don’t need saves both threads and time. page.route("**/*", handler) can abort requests for images, media and fonts on a scraping run; fewer requests per page means more pages fit in the same plan.
Rotation inside a browser: what to expect
On the Main gateway each new connection can get a new exit IP. A browser keeps connections alive and reuses them for the next requests to the same host, so one page, and often several pages in the same context, keeps the same IP for a while. That’s usually what you want: a page whose script calls come from different IPs than its HTML looks odd to the site.
For a clean new IP per task, open a fresh context per URL or batch and close it afterwards. For an IP that must hold through a multi-step session, don’t rely on connection reuse at all. Use the 3- or 15-minute gateway, or a residential port, where the hold time is defined. Residential IPs change at fixed minutes (1, 6, 11, 16 and so on), and open connections can drop at that moment, so retry a navigation that fails right on the switch.
Verify the proxy the right way
Check the exit IP from inside the page, not with a separate HTTP client, because only the page’s traffic goes through the context’s proxy. Load an IP echo endpoint in each context and log the result with the context name. For geography, compare against the gateway region you chose: USA, EU or the 50/50 mix.
Also check that nothing goes direct by accident. A bypass list catches localhost; anything else in it skips the proxy too. If your real IP ever shows up, look for a context created without a proxy while the browser had none at launch.
Running Playwright on servers and in CI
Rotating and residential gateways authenticate by the connecting IP. A VPS or dedicated server with a static IP is ideal: authorize it once and every browser on it can use the gateways. GitHub Actions, most hosted CI runners and serverless functions get a new outgoing IP per run, so they won’t be on your list. For those, run the browsers on a fixed-IP server you control, or use private dedicated proxies with the username and password fields shown above.
Common errors and fixes
net::ERR_PROXY_CONNECTION_FAILEDPlaywright couldn’t reach the gateway: wrong IP or port, or a firewall blocks outbound traffic to that port. Copy the gateway again from the member area. See this fix guide.net::ERR_TUNNEL_CONNECTION_FAILEDThe HTTPS tunnel wasn’t opened, most often because your current IP isn’t authorized yet (the gateway resets the connection) or you’re over your thread limit. Check Authorized IPs, wait up to 15 minutes after saving, and cut parallel pages. More in this guide.TimeoutError: page.goto: Timeout 30000ms exceededThe page didn’t reach the load event in time. Raise the navigation timeout to 60 seconds, use wait_until="domcontentloaded", and check you aren’t running more pages than your threads allow.407 Proxy Authentication RequiredOn a dedicated proxy, the username or password in the proxy dict is wrong. On rotating or residential there are no credentials to add, and an unauthorized IP shows as a connection error, not a 407; a 407 there comes from another proxy in the chain.new_context() call.finally block and cap the number of workers.FAQ
How many Playwright tabs can I run on a Storm rotating plan?
Divide your threads by 10: about 4 tabs on 40 threads, 8 on 80, 15 on 150 and 20 on 200. Blocking images and fonts lets you push a bit higher; streaming video or heavy single-page apps need more headroom.
Do I need a username and password for Storm proxies in Playwright?
Not for rotating or residential gateways: authorize the IP of the machine running Playwright and use only server. Private dedicated proxies can use either IP authorization or the username and password fields.
Can each browser context use a different proxy?
Yes. Pass proxy to new_context() (Python) or newContext() (Node.js). Each context keeps its own cookies and its own gateway, which is how you run several accounts or regions in one browser.
Does Playwright’s proxy work with Firefox and WebKit too?
Yes. The proxy option is part of the shared API for all three engines, in launch and in contexts. Thread use per page is similar across engines.
Why does the IP stay the same across several pages on a rotating gateway?
The browser reuses open connections, and the gateway picks the exit IP when a connection opens. Use a new context per task for fresh IPs, or a timed gateway when you need the IP to stay put on purpose.
Should I use the http:// or https:// scheme for the proxy server?
Use http://GATEWAY_IP:PORT. It describes the link to the proxy; https sites still load encrypted through a CONNECT tunnel. Storm gateways are HTTP(S) proxies only, so a socks5:// server won’t connect.
Still have questions? Contact us here. A real person answers.
Related guides
Tool facts checked against the official documentation (October 2026): Playwright: Network, HTTP proxy · Playwright Python: Browser.new_context · Playwright Python: Page.goto and timeouts · Playwright Node.js: BrowserType.launch. Storm Proxies facts: our plans page and refund policy.
Unlimited bandwidth. One flat monthly price.
Access is live the moment you pay, and the smallest package of each proxy type has a 24-hour money-back guarantee on your first order.