Save 5% every month: use code 5OFFSTORM at checkout
Home / Guides / Playwright

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.

Updated October 2026Code & scraping frameworks

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 plans

Before you start

  1. Log in to the member area and copy your gateway IP:PORTs. They never change; the rotation happens on our side.
  2. 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.
  3. 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.
  4. 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

  1. Install Playwright and a browser

    Python: pip install playwright, then playwright install chromium. Node.js: npm i playwright (or npm init playwright@latest for the test runner), then npx playwright install chromium.

  2. 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 in new_context(). You can combine them; a context’s own proxy overrides the launch one.

  3. Write the proxy as a dict

    The option is an object, not a URL string: {"server": "http://GATEWAY_IP:PORT"}. Add username and password keys only for dedicated proxies that use user:pass. Add bypass for hosts that should go direct, as a comma-separated string.

  4. Open a page and check the IP

    Go to https://httpbin.org/ip and 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.

  5. 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.

  6. 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 use wait_until="domcontentloaded" when you don’t need every image and tracker to finish.

  7. Run headless in production

    Chromium launches headless by default. Use headless=False while 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.

Python: launch-level proxy (sync API)
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()
Node.js: launch-level proxy
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();
})();
Python async: a different gateway per context
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())
Node.js: limit open pages to your plan
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();
})();
Dedicated proxy with username and password
# 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' },
});
Playwright Test (playwright.config.ts)
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.
Your own IP appears in a contextThat context has no proxy and the browser was launched without one. Set the proxy at launch as a safety net, or audit every new_context() call.
Navigation fails with a connection error on residential, every few minutesThe port’s IP rotated while the page was loading, and the open connection dropped. Wrap navigation in a retry and keep tasks short enough to finish between rotations.
Proxy works for the first pages, then errors pile upPages aren’t being closed, so connections accumulate past your plan. Close each page in a 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.