How to Use Proxies with Playwright in 2026: Complete Guide

How to Use Proxies with Playwright in 2026: Complete Guide

Playwright has become the go-to browser automation framework for developers who need reliable, cross-browser testing and web scraping. But running Playwright without proxies is like driving without a seatbelt — you might be fine for a while, but eventually you will hit a wall of IP bans, CAPTCHAs, and geo-restrictions.

This guide covers everything you need to know about integrating proxies with Playwright in 2026, with real, copy-paste-ready code examples in both Node.js and Python.

Why Use Proxies with Playwright?

Playwright launches real browser instances (Chromium, Firefox, WebKit), which means target websites see a full browser fingerprint. However, they also see your IP address. Proxies solve several problems:

  • IP rotation prevents rate limiting and bans during scraping
  • Geo-targeting lets you access region-locked content
  • Anonymity keeps your infrastructure IPs hidden
  • Parallel scaling allows hundreds of concurrent sessions without IP conflicts

If you are unsure how much proxy bandwidth your project will consume, use our proxy cost calculator to estimate costs before committing to a provider.

Prerequisites

Before starting, make sure you have:

  • Node.js 18+ or Python 3.9+ installed
  • Playwright installed in your project
  • Access to a proxy provider (HTTP, HTTPS, or SOCKS5)

Install Playwright (Node.js)

npm init -y
npm install playwright
npx playwright install

Install Playwright (Python)

pip install playwright
playwright install

Basic Proxy Setup

Node.js — Browser-Level Proxy

The simplest way to use a proxy with Playwright is to pass it when launching the browser:

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({
    proxy: {
      server: 'http://proxy.example.com:8080'
    }
  });

  const page = await browser.newPage();
  await page.goto('https://httpbin.org/ip');
  const content = await page.textContent('body');
  console.log('Response:', content);

  await browser.close();
})();

Python — Browser-Level Proxy

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(proxy={
        "server": "http://proxy.example.com:8080"
    })

    page = browser.new_page()
    page.goto("https://httpbin.org/ip")
    print(page.text_content("body"))

    browser.close()

Authenticated Proxy Setup

Most commercial proxy providers require username and password authentication.

Node.js — Authenticated Proxy

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({
    proxy: {
      server: 'http://proxy.example.com:8080',
      username: 'your_username',
      password: 'your_password'
    }
  });

  const page = await browser.newPage();
  await page.goto('https://httpbin.org/ip');
  console.log(await page.textContent('body'));

  await browser.close();
})();

Python — Authenticated Proxy

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(proxy={
        "server": "http://proxy.example.com:8080",
        "username": "your_username",
        "password": "your_password"
    })

    page = browser.new_page()
    page.goto("https://httpbin.org/ip")
    print(page.text_content("body"))

    browser.close()

SOCKS5 Proxy Support

Playwright supports SOCKS5 proxies natively. Simply change the protocol in the server URL:

const browser = await chromium.launch({
  proxy: {
    server: 'socks5://proxy.example.com:1080',
    username: 'user',
    password: 'pass'
  }
});
browser = p.chromium.launch(proxy={
    "server": "socks5://proxy.example.com:1080",
    "username": "user",
    "password": "pass"
})

Per-Context Proxy Configuration

One of Playwright’s most powerful features is browser contexts. You can assign a different proxy to each context, allowing multiple identities within a single browser instance.

Node.js — Proxy Per Context

const { chromium } = require('playwright');

(async () => {
  // Launch browser with a per-context proxy server
  // The browser must be launched with proxy config for contexts to use it
  const browser = await chromium.launch({
    proxy: {
      server: 'per-context'
    }
  });

  // Context 1: US proxy
  const usContext = await browser.newContext({
    proxy: {
      server: 'http://us-proxy.example.com:8080',
      username: 'user',
      password: 'pass'
    }
  });

  // Context 2: UK proxy
  const ukContext = await browser.newContext({
    proxy: {
      server: 'http://uk-proxy.example.com:8080',
      username: 'user',
      password: 'pass'
    }
  });

  const usPage = await usContext.newPage();
  const ukPage = await ukContext.newPage();

  await Promise.all([
    usPage.goto('https://httpbin.org/ip'),
    ukPage.goto('https://httpbin.org/ip')
  ]);

  console.log('US IP:', await usPage.textContent('body'));
  console.log('UK IP:', await ukPage.textContent('body'));

  await browser.close();
})();

Python — Proxy Per Context

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(proxy={"server": "per-context"})

    us_context = browser.new_context(proxy={
        "server": "http://us-proxy.example.com:8080",
        "username": "user",
        "password": "pass"
    })

    uk_context = browser.new_context(proxy={
        "server": "http://uk-proxy.example.com:8080",
        "username": "user",
        "password": "pass"
    })

    us_page = us_context.new_page()
    uk_page = uk_context.new_page()

    us_page.goto("https://httpbin.org/ip")
    uk_page.goto("https://httpbin.org/ip")

    print("US IP:", us_page.text_content("body"))
    print("UK IP:", uk_page.text_content("body"))

    browser.close()

Rotating Proxies with Playwright

For scraping at scale, you need proxy rotation. Here are two strategies.

Strategy 1: Rotate via New Contexts

Create a fresh context with a different proxy for each task:

const { chromium } = require('playwright');

const proxies = [
  { server: 'http://proxy1.example.com:8080', username: 'user', password: 'pass' },
  { server: 'http://proxy2.example.com:8080', username: 'user', password: 'pass' },
  { server: 'http://proxy3.example.com:8080', username: 'user', password: 'pass' },
];

(async () => {
  const browser = await chromium.launch({ proxy: { server: 'per-context' } });
  const urls = [
    'https://example.com/page1',
    'https://example.com/page2',
    'https://example.com/page3',
  ];

  for (let i = 0; i < urls.length; i++) {
    const proxy = proxies[i % proxies.length];
    const context = await browser.newContext({ proxy });
    const page = await context.newPage();

    try {
      await page.goto(urls[i], { timeout: 30000 });
      const title = await page.title();
      console.log(`[${proxy.server}] ${urls[i]} -> ${title}`);
    } catch (err) {
      console.error(`Failed on ${urls[i]}: ${err.message}`);
    } finally {
      await context.close();
    }
  }

  await browser.close();
})();

Strategy 2: Use a Rotating Proxy Gateway

Most proxy providers offer a single gateway endpoint that automatically rotates IPs on each request. This is the simplest approach:

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({
    proxy: {
      server: 'http://gate.smartproxy.com:7777',
      username: 'user-session-abc123',
      password: 'password'
    }
  });

  // Each new page request gets a different IP from the gateway
  for (let i = 0; i < 5; i++) {
    const context = await browser.newContext();
    const page = await context.newPage();
    await page.goto('https://httpbin.org/ip');
    console.log(`Request ${i + 1}:`, await page.textContent('body'));
    await context.close();
  }

  await browser.close();
})();

Handling Proxy Failures and Retries

Proxies fail. Connections drop, proxies get banned, timeouts happen. You need retry logic:

const { chromium } = require('playwright');

async function fetchWithRetry(browser, url, proxies, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const proxy = proxies[attempt % proxies.length];
    const context = await browser.newContext({ proxy });
    const page = await context.newPage();

    try {
      const response = await page.goto(url, {
        timeout: 20000,
        waitUntil: 'domcontentloaded'
      });

      if (response && response.status() === 200) {
        const content = await page.content();
        await context.close();
        return content;
      }

      console.warn(`Attempt ${attempt + 1}: HTTP ${response?.status()}`);
    } catch (err) {
      console.warn(`Attempt ${attempt + 1} failed: ${err.message}`);
    } finally {
      await context.close();
    }
  }

  throw new Error(`All ${maxRetries} attempts failed for ${url}`);
}

// Usage
(async () => {
  const proxies = [
    { server: 'http://proxy1.example.com:8080', username: 'user', password: 'pass' },
    { server: 'http://proxy2.example.com:8080', username: 'user', password: 'pass' },
    { server: 'http://proxy3.example.com:8080', username: 'user', password: 'pass' },
  ];

  const browser = await chromium.launch({ proxy: { server: 'per-context' } });

  try {
    const html = await fetchWithRetry(browser, 'https://example.com', proxies);
    console.log('Success, got', html.length, 'bytes');
  } catch (err) {
    console.error(err.message);
  }

  await browser.close();
})();

Combining Playwright with Stealth Plugins

Websites use fingerprinting to detect automation. Playwright itself is more stealthy than Puppeteer out of the box, but you can improve it further.

Using playwright-extra and Stealth Plugin (Node.js)

npm install playwright-extra puppeteer-extra-plugin-stealth
const { chromium } = require('playwright-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

chromium.use(StealthPlugin());

(async () => {
  const browser = await chromium.launch({
    proxy: {
      server: 'http://proxy.example.com:8080',
      username: 'user',
      password: 'pass'
    },
    headless: true
  });

  const context = await browser.newContext({
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
    viewport: { width: 1920, height: 1080 },
    locale: 'en-US',
    timezoneId: 'America/New_York'
  });

  const page = await context.newPage();

  // Block unnecessary resources to save bandwidth
  await page.route('**/*.{png,jpg,jpeg,gif,svg,woff,woff2,ttf}', route => route.abort());

  await page.goto('https://bot.sannysoft.com');
  await page.screenshot({ path: 'stealth-test.png', fullPage: true });

  await browser.close();
})();

To verify your browser fingerprint setup is effective, test it against our browser fingerprint tester.

Manual Stealth Tweaks (Python)

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(
        proxy={
            "server": "http://proxy.example.com:8080",
            "username": "user",
            "password": "pass"
        },
        headless=True
    )

    context = browser.new_context(
        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
        viewport={"width": 1920, "height": 1080},
        locale="en-US",
        timezone_id="America/New_York",
        permissions=["geolocation"],
        geolocation={"latitude": 40.7128, "longitude": -74.0060},
    )

    page = context.new_page()

    # Override webdriver property
    page.add_init_script("""
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });
    """)

    page.goto("https://httpbin.org/ip")
    print(page.text_content("body"))

    browser.close()

Performance Tips

1. Reuse Browser Instances

Launching a browser is expensive. Launch once and create multiple contexts:

// Good: One browser, many contexts
const browser = await chromium.launch({ proxy: { server: 'per-context' } });

for (const task of tasks) {
  const context = await browser.newContext({ proxy: task.proxy });
  // ... do work ...
  await context.close(); // Close context, not browser
}

await browser.close(); // Close browser when all done

2. Block Unnecessary Resources

Save proxy bandwidth by blocking images, fonts, and tracking scripts:

await page.route('**/*', (route) => {
  const resourceType = route.request().resourceType();
  if (['image', 'font', 'media', 'stylesheet'].includes(resourceType)) {
    return route.abort();
  }
  return route.continue();
});

3. Connection Pooling with Concurrency Control

const { chromium } = require('playwright');

async function scrapeWithPool(urls, proxies, concurrency = 5) {
  const browser = await chromium.launch({ proxy: { server: 'per-context' } });
  const results = [];
  let index = 0;

  async function worker() {
    while (index < urls.length) {
      const currentIndex = index++;
      const url = urls[currentIndex];
      const proxy = proxies[currentIndex % proxies.length];

      const context = await browser.newContext({ proxy });
      const page = await context.newPage();

      try {
        await page.goto(url, { timeout: 20000 });
        results[currentIndex] = {
          url,
          title: await page.title(),
          status: 'success'
        };
      } catch (err) {
        results[currentIndex] = { url, error: err.message, status: 'failed' };
      } finally {
        await context.close();
      }
    }
  }

  const workers = Array.from({ length: concurrency }, () => worker());
  await Promise.all(workers);
  await browser.close();

  return results;
}

4. Use Persistent Contexts for Session Stickiness

When you need to maintain cookies and session data across proxy reconnections:

const context = await chromium.launchPersistentContext('/tmp/playwright-session', {
  proxy: {
    server: 'http://sticky-proxy.example.com:8080',
    username: 'user-session-fixed123',
    password: 'pass'
  }
});

Troubleshooting Common Issues

Error: net::ERR_PROXY_CONNECTION_FAILED

Cause: The proxy server is unreachable or the address is wrong.

Fix: Verify the proxy is online by testing with cURL first:

curl -x http://proxy.example.com:8080 https://httpbin.org/ip

Error: net::ERR_TUNNEL_CONNECTION_FAILED

Cause: The proxy does not support HTTPS tunneling (CONNECT method).

Fix: Use an HTTPS-capable proxy or switch to SOCKS5.

Error: Timeout waiting for page to load

Cause: Slow proxy or proxy bandwidth throttling.

Fix: Increase the timeout or switch to a faster proxy:

await page.goto(url, { timeout: 60000, waitUntil: 'domcontentloaded' });

Proxy Authentication Popup Appears in Headed Mode

Cause: Some proxies trigger a browser auth dialog instead of using the configured credentials.

Fix: Ensure you pass username and password in the proxy config. If using headed mode for debugging, Playwright handles this automatically when credentials are provided.

WebSocket Connections Bypass Proxy

Cause: By default, Playwright routes WebSocket traffic through the proxy, but some configurations may not.

Fix: Make sure your proxy supports WebSocket tunneling, or use page.route() to intercept and redirect WebSocket requests.

Summary

Playwright’s built-in proxy support is one of the best among browser automation frameworks. The key features that make it stand out:

  • Native proxy configuration at both browser and context levels
  • Per-context proxy switching without relaunching the browser
  • SOCKS5 support out of the box
  • Authentication handling without browser extensions

For any serious scraping or automation project, combine Playwright with rotating residential proxies, stealth plugins, and proper error handling. Use our proxy cost calculator to estimate your bandwidth needs, and test your setup with the browser fingerprint tester before going to production.


Related Reading

Scroll to Top