     [Blog](https://scrapfly.io/blog)   /  [playwright](https://scrapfly.io/blog/tag/playwright)   /  [How to Use Proxies with Playwright: Authentication, Rotation, and Per-Context Proxies](https://scrapfly.io/blog/posts/how-to-use-proxies-with-playwright)   # How to Use Proxies with Playwright: Authentication, Rotation, and Per-Context Proxies

 by [Ziad Shamndy](https://scrapfly.io/blog/author/ziad) Nov 10, 2026 27 min read [\#playwright](https://scrapfly.io/blog/tag/playwright) [\#proxies](https://scrapfly.io/blog/tag/proxies) [\#python](https://scrapfly.io/blog/tag/python) 

 [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-playwright "Share on LinkedIn") [  ](https://x.com/intent/tweet?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-playwright&text=How%20to%20Use%20Proxies%20with%20Playwright%3A%20Authentication%2C%20Rotation%2C%20and%20Per-Context%20Proxies "Share on X") [  ](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-playwright "Share on Facebook")    

 

 

Summarize this article with

 [  ](https://chat.openai.com/?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-playwright) [  ](https://claude.ai/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-playwright) [  ](https://x.com/i/grok?text=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-playwright) [  ](https://www.perplexity.ai/search/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-playwright) [  ](https://www.google.com/search?udm=50&aep=11&q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-playwright) 



         

Proxies are essential for scraping with Playwright, as they spread browser traffic across different IP addresses. But what happens when the proxy password is wrong? In our tests, `page.goto()` waited 30 seconds and raised a timeout, with no 407 status.

In this article, we'll explain how to use proxies in Playwright. We'll start with the basic proxy setup and authentication, then move on to per-context proxies, rotation, SOCKS5 and matching the browser locale to the proxy country. Let's dive in!



## Key Takeaways

Here's what happened when we tested proxies in Playwright for this guide:

- With a wrong proxy password, the page just waited for 30 seconds and timed out. Playwright never showed us the 407 error.
- The proxy login only worked in the username and password fields. Putting it in the proxy URL didn't work.
- Each browser context took its own proxy, and the other proxy never saw that context's requests.
- Our proxy carried the browser's traffic, but the browser still said HeadlessChrome and used our own timezone.
- Scrapfly [Cloud Browser](https://scrapfly.io/products/cloud-browser-api) ran the same script from a German residential IP, with the Berlin timezone and no HeadlessChrome in the user agent.

**Get web scraping tips in your inbox**Trusted by 100K+ developers and 30K+ enterprises. Unsubscribe anytime.







## How Do You Set a Proxy in Playwright?

Playwright is a cross-platform and cross-language web browser automation toolkit, and the proxy is one of its launch options. We can assign a proxy IP address per Playwright browser basis by passing a `proxy` dictionary to `chromium.launch()`.

The Playwright [network docs](https://playwright.dev/python/docs/network) say the proxy "can be either set globally for the entire browser, or for each browser context individually". We'll start with the global one.

There are two major proxy protocols used these days, HTTP and SOCKS (latest SOCKS5). Playwright takes both in the `server` value as `http://`, `https://` or `socks5://`, and a plain `host:port` without a scheme is considered an HTTP proxy.

Playwright for Python can be installed through pip, and we'll also need a Chromium build for the examples. For the rest of the Playwright API, see our [Playwright scraping basics](https://scrapfly.io/blog/posts/web-scraping-with-playwright-and-python) guide.

shell

```shell
pip install playwright
playwright install chromium
```



To start, we need to launch a browser with the proxy and open a new browser tab. Our first proxy is allowlisted by IP, so it doesn't require a username or password.

We read the proxy address from the `PROXY_SERVER` environment variable. Then, we use `page.goto()` to direct the browser to httpbin.dev/anything, which returns the IP address it sees in the `origin` field:

python

```python
import json
import os

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(proxy={"server": os.environ["PROXY_SERVER"]})
    try:
        page = browser.new_page()
        page.goto("https://httpbin.dev/anything")
        data = json.loads(page.inner_text("body"))
        print("origin:", data["origin"])
    finally:
        browser.close()
```



Here is what we got:

```
origin: xxx.xxx.xxx.xxx
```



We masked the IP, as our test proxy was running on our own machine. The proxy log shows the browser's connection to httpbin.dev for this run:

```
CONNECT httpbin.dev:443 - 8113 bytes - 901.62ms
```



The option has the same name and shape in Node.js, so the JavaScript version is `chromium.launch({ proxy: { server } })`.

### How Do You Check That Playwright Is Using the Proxy?

The `origin` value from httpbin.dev is the easiest check. So, **if it's different from the IP address we get without the proxy, the proxy is applied**.

The same IP address on both checks doesn't prove the proxy was skipped, though. A local forwarder, like our test proxy, exits through the same IP as a direct connection:

python

```python
import json
import os

from playwright.sync_api import sync_playwright


def get_origin(p, **options):
    browser = p.chromium.launch(**options)
    try:
        page = browser.new_page()
        page.goto("https://httpbin.dev/anything")
        return json.loads(page.inner_text("body"))["origin"]
    finally:
        browser.close()


with sync_playwright() as p:
    direct = get_origin(p)
    proxied = get_origin(p, proxy={"server": os.environ["PROXY_SERVER"]})
    print("same IP:", direct == proxied)
```



Here is what we got:

```
same IP: True
```



Both runs returned the same IP address, and yet the proxy log shows the second run going through it:

```
CONNECT httpbin.dev:443 - 7363 bytes - 2158.89ms
```



So, **when the two IP addresses match, we check the proxy logs or compare against a provider with a different exit IP**.

When the proxy isn't reachable at all, Playwright doesn't fall back to a direct connection. We pointed the same script at a port with nothing running on it, and `page.goto()` raised this error:

```
playwright._impl._errors.Error: Page.goto: net::ERR_PROXY_CONNECTION_FAILED at https://httpbin.dev/anything
Call log:
  - navigating to "https://httpbin.dev/anything", waiting until "load"
```



Note that the check has to run in the browser. A `requests` call in the same script doesn't use Playwright's proxy settings.

For a manual check, our [IP info tool](https://scrapfly.io/web-scraping-tools/ip-info) shows the same details. For more on proxy types and how they compare, see our [introduction to proxies in web scraping](https://scrapfly.io/blog/posts/introduction-to-proxies-in-web-scraping).

Having a set of **quality proxies can prevent web scraping blocking**. Many of them require a username and password though, so let's take a look at proxy authentication next.



## How Do You Authenticate a Proxy in Playwright?

When scraping at scale we might need to use proxies to prevent blocking. Most of these proxies require a username and password, which can be added through the same `proxy` parameter:

python

```python
import json
import os

from playwright.sync_api import sync_playwright

proxy = {
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USER"],
    "password": os.environ["PROXY_PASS"],
}

with sync_playwright() as p:
    browser = p.chromium.launch(proxy=proxy)
    try:
        page = browser.new_page()
        response = page.goto("https://httpbin.dev/anything", timeout=10_000)
        data = json.loads(page.inner_text("body"))
        print("status:", response.status)
        print("origin:", data["origin"])
    finally:
        browser.close()
```



In the above code, we define the proxy `server`, `username` and `password` using environment variables. Then, we pass them to `chromium.launch()` and define a timeout of 10 seconds for `page.goto()`.

Here's what the script printed:

```
status: 200
origin: xxx.xxx.xxx.xxx
```



Using proxies in Playwright for web scraping allows us to distribute our requests' traffic across different IP addresses. This will make it harder for websites to track the IP address origin, preventing IP address throttling and blocking.

Our proxy log shows that the first request was sent without credentials and rejected. Then, the browser retried with the username and password:

```
[W] HttpProtocolException: ProxyAuthenticationFailed
[I] 127.0.0.1:47730 - CONNECT None:None - 0 bytes - 0.63ms
[I] 127.0.0.1:47732 - CONNECT httpbin.dev:443 - 8200 bytes - 834.84ms
```



In cURL, proxies with credentials are added to the URL as `username:password@`. Playwright didn't use credentials from the `server` URL in our test, and the request timed out after 30 seconds.

### Why Does page.goto() Time Out Instead of Showing a 407?

HTTP requests can fail. Be it connection loss, server error, timeout or blocking it's important to handle these failures gracefully.

Unfortunately, with a wrong password, Playwright couldn't connect through the proxy and timed out:

```
playwright._impl._errors.TimeoutError: Page.goto: Timeout 30000ms exceeded.
Call log:
  - navigating to "https://httpbin.dev/anything", waiting until "load"
```



This error suggests a failure in the proxy authentication. The proxy log shows two rejected attempts before the timeout:

```
[W] HttpProtocolException: ProxyAuthenticationFailed
[I] 127.0.0.1:35784 - CONNECT None:None - 0 bytes - 0.84ms
[W] HttpProtocolException: ProxyAuthenticationFailed
[I] 127.0.0.1:35794 - CONNECT None:None - 0 bytes - 0.97ms
```



The `TimeoutError` error occurs when a request takes longer than the specified/default timeout duration. Raising the timeout won't help here though, as the proxy already rejected our credentials.

To add proxies for cURL, we can use the `-x` or `--proxy` option followed by the proxy URL. Let's send the same request with cURL and the wrong password:

shell

```shell
curl --max-time 20 -v -x "$PROXY_SERVER" -U "$PROXY_USER:wrong-password" https://httpbin.dev/anything 2>&1 | grep -E "407|Proxy-Authenticate"
```



```
< HTTP/1.1 407 Proxy Authentication Required
< Proxy-Authenticate: Basic
* CONNECT tunnel failed, response 407
curl: (7) CONNECT tunnel failed, response 407
```



As we can see, the proxy returns the 407 Proxy Authentication Required status, which Playwright never showed us.

In Python, Playwright failures can be handled with `try/except`. So, **we set a short timeout on the first page load and catch the `TimeoutError`**:

python

```python
import os

from playwright.sync_api import TimeoutError, sync_playwright


def check_proxy(proxy):
    with sync_playwright() as p:
        browser = p.chromium.launch(proxy=proxy)
        try:
            page = browser.new_page()
            page.goto("https://httpbin.dev/anything", timeout=10_000)
            print("proxy works:", proxy["server"])
        except TimeoutError:
            print("timed out: check the proxy username and password first")
        finally:
            browser.close()


check_proxy({
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USER"],
    "password": os.environ["PROXY_PASS"],
})
```



In the above code, we define a timeout of 10 seconds using the `timeout` parameter of `page.goto()`. Then, we catch the `TimeoutError` and print a hint about the credentials. Here's what we got with working credentials:

```
proxy works: http://127.0.0.1:8899
```



And with a wrong password, after about 10 seconds:

```
timed out: check the proxy username and password first
```



For more details on the 407 status code, refer to our [407 Proxy Authentication Required](https://scrapfly.io/blog/posts/what-is-http-407-status-code-and-how-to-fix-it) guide. Next, let's take a look at using a different proxy for each browser context.



## How Do You Use a Different Proxy per Browser Context?

So far, our proxy was set for the whole browser. However, Playwright can also set the proxy for each browser context individually, through the `proxy` parameter of `browser.new_context()`.

It's important to **distribute web scraper traffic through multiple agents**. This is done by creating multiple profiles with proxies, header details and other settings. In Playwright, each of these profiles can be a browser context.

Using context, we can define page properties like viewport dimensions, and the proxy is one of them. Each context also has its own cookies and storage, so one browser can run several sessions at once:

python

```python
import json
import os

from playwright.sync_api import sync_playwright

proxies = [
    {"server": os.environ["PROXY_A"], "username": os.environ["PROXY_A_USER"], "password": os.environ["PROXY_A_PASS"]},
    {"server": os.environ["PROXY_B"], "username": os.environ["PROXY_B_USER"], "password": os.environ["PROXY_B_PASS"]},
]

with sync_playwright() as p:
    # create browser instance without any proxy
    browser = p.chromium.launch()
    try:
        for proxy in proxies:
            # create context
            # using context we can define the proxy and its credentials
            context = browser.new_context(proxy=proxy)
            try:
                # create page aka browser tab inside this context
                page = context.new_page()
                page.goto("https://httpbin.dev/anything", timeout=15_000)
                data = json.loads(page.inner_text("body"))
                print(proxy["server"], "->", data["origin"])
            finally:
                context.close()
    finally:
        browser.close()
```



In the above code, we start by launching a browser instance without any proxy. Then, we create a context for each proxy, open a page aka browser tab inside it and print the IP address httpbin.dev sees.

Here's what the script printed:

```
http://127.0.0.1:8899 -> xxx.xxx.xxx.xxx
http://127.0.0.1:8898 -> xxx.xxx.xxx.xxx
```



We masked the IPs again, as both test proxies were running on our machine. Each proxy logged its own CONNECT request to httpbin.dev:

```
# proxy A
CONNECT None:None - 0 bytes - 16.50ms
CONNECT httpbin.dev:443 - 8200 bytes - 1200.14ms
# proxy B
CONNECT None:None - 0 bytes - 38.67ms
CONNECT httpbin.dev:443 - 8200 bytes - 1005.99ms
```



Next, we opened three contexts at the same time, two with these proxies and one without a proxy. The first loaded httpbin.dev, the second web-scraping.dev/products and the third httpbin.dev/headers:

```
# proxy A
CONNECT httpbin.dev:443 - 8238 bytes - 4070.16ms
# proxy B
CONNECT web-scraping.dev:443 - 589775 bytes - 3166.84ms
```



As we can see, each proxy only got the traffic of its own context. The plain context's request didn't show up in either log, as it connected directly.

Note that pages can't have their own proxy, as the context is the smallest unit Playwright sets it on. So, **for a proxy per page, we open one page per context**.

Since a context keeps its proxy until it's closed, creating new contexts is also how we rotate proxies in Playwright.



## How Do You Rotate Proxies in Playwright?

When web scraping with a pool of proxies our connection success chances can be influenced by connection patterns. In other words, if proxy A connects to 50 pages in 5 seconds it's likely to get throttled or blocked.

In Playwright, the proxy is set when a context is created. So, to rotate proxies we open a new context with the next proxy, either from our own list or through a rotating gateway.

### Rotating Gateway or Proxy List: Which Pattern Fits?

A rotating gateway is one proxy address where the provider changes the IP for us, and the authenticated launch from earlier works as it is. We didn't have one for this guide, so we tested a proxy list instead.

Our list had three proxies, and the second one was dead on purpose:

python

```python
import itertools
import os

from playwright.sync_api import Error, sync_playwright


def parse_proxy(entry):
    # turn "user:pass@host:port" into a Playwright proxy dictionary
    credentials, host = entry.rsplit("@", 1)
    username, password = credentials.split(":", 1)
    return {"server": f"http://{host}", "username": username, "password": password}


# load our proxy list and cycle through it
proxies = [parse_proxy(p) for p in os.environ["PROXY_LIST"].split(",")]
proxy_pool = itertools.cycle(proxies)


def scrape(browser, url, attempts=3):
    for _ in range(attempts):
        proxy = next(proxy_pool)
        # every attempt gets a new context with the next proxy
        context = browser.new_context(proxy=proxy)
        try:
            page = context.new_page()
            response = page.goto(url, timeout=15_000)
            if response and response.ok:
                print(url, "served by", proxy["server"])
                return page.content()
            print(proxy["server"], "returned", response.status if response else None)
        except Error as error:
            # dead or blocked proxy, try the next one
            print(proxy["server"], "failed:", str(error).splitlines()[0])
        finally:
            context.close()
    raise RuntimeError(f"all {attempts} attempts failed for {url}")


with sync_playwright() as pw:
    browser = pw.chromium.launch()
    try:
        for n in range(1, 4):
            scrape(browser, f"https://web-scraping.dev/product/{n}")
    finally:
        browser.close()
```



Here is what we got:

```
https://web-scraping.dev/product/1 served by http://127.0.0.1:8899
http://127.0.0.1:8896 failed: Page.goto: net::ERR_PROXY_CONNECTION_FAILED at https://web-scraping.dev/product/2
https://web-scraping.dev/product/2 served by http://127.0.0.1:8898
https://web-scraping.dev/product/3 served by http://127.0.0.1:8899
```



Product 2 landed on the dead proxy and failed right away with `net::ERR_PROXY_CONNECTION_FAILED`. Then, the script opened a new context on the next proxy and the page loaded.

For weighting proxies and tracking their performance, see our [proxy rotation strategy](https://scrapfly.io/blog/posts/how-to-rotate-proxies-in-web-scraping) guide.

### When Should a Playwright Session Keep a Sticky IP?

To see what a session looks like, we logged in to web-scraping.dev in a context on proxy A. Then, we opened the same login page in a second context on proxy B:

python

```python
import os

from playwright.sync_api import sync_playwright

proxy_a = {"server": os.environ["PROXY_A"], "username": os.environ["PROXY_A_USER"], "password": os.environ["PROXY_A_PASS"]}
proxy_b = {"server": os.environ["PROXY_B"], "username": os.environ["PROXY_B_USER"], "password": os.environ["PROXY_B_PASS"]}

with sync_playwright() as pw:
    browser = pw.chromium.launch()
    try:
        # log in through proxy A
        context_a = browser.new_context(proxy=proxy_a)
        page = context_a.new_page()
        page.goto("https://web-scraping.dev/login")
        page.fill("#username", "user123")
        page.fill("#password", "password")
        page.click("button[type=submit]")
        page.wait_for_load_state()
        print("proxy A cookies:", [c["name"] for c in context_a.cookies()])

        # open the same page through proxy B
        context_b = browser.new_context(proxy=proxy_b)
        page = context_b.new_page()
        page.goto("https://web-scraping.dev/login")
        print("proxy B cookies:", [c["name"] for c in context_b.cookies()])
    finally:
        browser.close()
```



Here is what we got:

```
proxy A cookies: ['auth', 'login_ip']
proxy B cookies: []
```



The login gave us two cookies, and one of them is called `login_ip`. It holds the IP address we logged in from, while the context on proxy B had no cookies at all.

Keep a sticky IP for an IP-bound login or a multi-step task such as a cart or checkout. Keep that task in one context, and use the provider's sticky-session setting so new connections retain its exit IP. A fixed proxy gateway alone does not guarantee a fixed exit. Our two-context example only demonstrates cookie isolation; it does not test whether changing IP invalidates a reused login. Next, let's take a look at SOCKS5 proxies.



## Using SOCKS5 Proxies with Playwright

In the context of web scraping proxies, there isn't much practical difference between the HTTP and SOCKS protocols.

HTTP proxies are more widely adopted by web scraping proxy providers and the HTTP client libraries. Playwright supports both, but let's see what happens with a SOCKS5 proxy that needs a password:

python

```python
import os

from playwright.sync_api import sync_playwright

proxy = {
    "server": os.environ["SOCKS_SERVER"],
    "username": os.environ["PROXY_USER"],
    "password": os.environ["PROXY_PASS"],
}

with sync_playwright() as pw:
    browser = pw.chromium.launch(proxy=proxy)
    browser.close()
```



Here is what we got:

```
playwright._impl._errors.Error: BrowserType.launch: Browser does not support socks5 proxy authentication
```



Chromium didn't even start. Then, we removed the `username` and `password` keys and ran the same script again, and this time Chromium launched with the SOCKS5 proxy.

So, **if a SOCKS5 proxy requires a login, we use its HTTP version instead**, with the same `username` and `password` keys.

For more details on the protocol differences, refer to our [SOCKS5 vs HTTP proxies](https://scrapfly.io/blog/posts/https-vs-socks-proxies) comparison. Next, let's take a look at which browser requests go through the proxy.



## What Else Goes Through the Proxy, and What Doesn't?

Playwright tracks all of the background requests and responses the browser sends and receives. When a proxy is set, all of these requests go through it, and in our tests this included `localhost` too.

### Does Playwright Send localhost Traffic Through the Proxy?

Yes, it did in our test. To see this, we ran a small web server on port 8765 and loaded it through our authenticated proxy:

python

```python
import os

from playwright.sync_api import sync_playwright

proxy = {
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USER"],
    "password": os.environ["PROXY_PASS"],
}

with sync_playwright() as pw:
    browser = pw.chromium.launch(proxy=proxy)
    try:
        page = browser.new_page()
        page.goto("http://localhost:8765/")
        print(page.inner_text("h1"))
    finally:
        browser.close()
```



Here is what we got:

```
local test app
```



The page loaded fine, and the proxy log shows that the request went through the proxy, even though the server runs on our own machine:

```
[W] HttpProtocolException: ProxyAuthenticationFailed
[I] 127.0.0.1:35400 - GET None:None/ - None None - 0 bytes - 0.88ms
[I] 127.0.0.1:35416 - GET localhost:8765/ - 200 OK - 236 bytes - 7.39ms
```



Playwright allows **limiting the proxy scope with the `bypass` option**, which takes a comma-separated list of hosts that connect directly:

python

```python
proxy = {
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USER"],
    "password": os.environ["PROXY_PASS"],
    # keep local services off the proxy
    "bypass": "localhost,127.0.0.1",
}
```



This time, the proxy log stayed empty and our local server got the request directly:

```
127.0.0.1 - - [28/Oct/2026 13:15:12] "GET / HTTP/1.1" 200 -
```



We also loaded web-scraping.dev/products through the proxy, and the proxy log had a single tunnel, to web-scraping.dev itself.

### Do Proxy Credentials Reach the Websites You Visit?

No, they didn't in our test. We printed the header names that httpbin.dev/headers received through the proxy:

python

```python
import json
import os

from playwright.sync_api import sync_playwright

proxy = {
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USER"],
    "password": os.environ["PROXY_PASS"],
}

with sync_playwright() as pw:
    browser = pw.chromium.launch(proxy=proxy)
    try:
        page = browser.new_page()
        page.goto("https://httpbin.dev/headers")
        headers = json.loads(page.inner_text("body"))["headers"]
        print(sorted(headers))
    finally:
        browser.close()
```



Here is what we got:

```
['Accept', 'Accept-Encoding', 'Cache-Control', 'Host', 'Pragma', 'Priority', 'Sec-Ch-Ua', 'Sec-Ch-Ua-Mobile', 'Sec-Ch-Ua-Platform', 'Sec-Fetch-Dest', 'Sec-Fetch-Mode', 'Sec-Fetch-Site', 'Sec-Fetch-User', 'Upgrade-Insecure-Requests', 'User-Agent']
```



There's no `Authorization` or `Proxy-Authorization` header in the list, so the proxy credentials stayed with the proxy.

Websites with HTTP authentication need the `http_credentials` option of the context instead. Here, we tried it on the httpbin.dev Basic auth endpoint with and without our proxy:

python

```python
import os

from playwright.sync_api import sync_playwright

proxy = {
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USER"],
    "password": os.environ["PROXY_PASS"],
}
url = "https://httpbin.dev/basic-auth/user/passwd"

with sync_playwright() as pw:
    browser = pw.chromium.launch()
    try:
        for name, extra in [("no proxy", {}), ("with proxy", {"proxy": proxy})]:
            context = browser.new_context(
                http_credentials={"username": "user", "password": "passwd"},
                **extra,
            )
            response = context.new_page().goto(url, timeout=15_000)
            print(name, "->", response.status)
            context.close()
    finally:
        browser.close()
```



Here is what we got:

```
no proxy -> 200
with proxy -> 401
```



Unfortunately, the same credentials got a 401 response once the proxy was set. We got the same 401 with the proxy set at launch instead of on the context. Next, let's take a look at proxy bandwidth.



Scrapfly

#### Need a cloud browser for scraping?

Run headless browsers at scale with Scrapfly Cloud Browser — no infrastructure to manage.

[Try Free →](https://scrapfly.io/register)## How Do You Cut Residential Proxy Bandwidth in Playwright?

Web scraping using headless browsers is really bandwidth intensive. The browser is downloading all of the images, fonts and other expensive resources our web scraper doesn't care about.

Therefore, it's essential to configure browser automation setups with resource blocking rules and proper caching rules to prevent bandwidth overhead, but generally expect browser traffic to be much more expensive bandwidth-wise.

To optimize this we can configure our Playwright instance to block these unnecessary resources. For this, we use `page.route()` and abort the requests with the image, font or media resource type.

We tried this on the web-scraping.dev products page through our test proxy. Without blocking, the proxy log showed 588,834 bytes for the page and 467,828 bytes with the five product images blocked.

Scrapfly [Proxy Saver](https://scrapfly.io/docs/proxy-saver/getting-started) does this on the proxy side, in front of our existing provider.

For more blocking rules, see our answer on how to [block resources in Playwright](https://scrapfly.io/blog/answers/how-to-block-resources-in-playwright) and our guide on [image and CSS stubbing](https://scrapfly.io/blog/posts/optimize-proxy-bandwidth-with-image-and-css-stubbing). Next, let's see why Playwright still gets blocked.



## Why Does Playwright Still Get Blocked Behind a Working Proxy?

Fingerprinting tech can be powerful enough to instantly identify a web scraper. Unfortunately, many web-browser automation tools leak information about themselves to javascript execution context, meaning javascript can easily tell the browser is being controlled by a program.

Let's see what our browser leaks from behind the proxy:

python

```python
import json
import os

from playwright.sync_api import sync_playwright

proxy = {
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USER"],
    "password": os.environ["PROXY_PASS"],
}

with sync_playwright() as pw:
    browser = pw.chromium.launch(proxy=proxy)
    try:
        page = browser.new_page()
        page.goto("https://httpbin.dev/headers")
        headers = json.loads(page.inner_text("body"))["headers"]
        print("User-Agent:", headers["User-Agent"])
        print("Accept-Language:", headers.get("Accept-Language"))
        print("webdriver:", page.evaluate("navigator.webdriver"))
        print("languages:", page.evaluate("navigator.languages"))
        print("timezone:", page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone"))
    finally:
        browser.close()
```



Here is what we got:

```
User-Agent: ['Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36']
Accept-Language: None
webdriver: True
languages: ['en-US']
timezone: Africa/Cairo
```



For example, the most commonly known leak when it comes to browser automation tools like Selenium, Playwright or Puppeteer is the `navigator.webdriver` leak where an automated browser would have `navigator.webdriver` value set to an unusual value of `true`.

Our browser had it set to `True` too, and the user agent even has `HeadlessChrome` in it. These variables can be read by any website making identification of robots extremely easy!

For more on how these values are used, see [how anti-bot detection works](https://scrapfly.io/blog/posts/how-anti-bot-detection-works) and our [bypass guides](https://scrapfly.io/bypass). For patching the webdriver leak, see our [Playwright Stealth](https://scrapfly.io/blog/posts/playwright-stealth-bypass-bot-detection) guide.

### How Do You Match Locale and Timezone to the Proxy Country?

The `Accept-Language` header indicates what language browser supports. When scraping non-english websites we might need to adjust this value to appropriate language to avoid standing out from the crowd.

Our browser didn't send this header at all, and its timezone was our machine's own. To fix this, we set both on a new context, then open the page from that context and navigate there:

python

```python
import json
import os

from playwright.sync_api import sync_playwright

proxy = {
    "server": os.environ["PROXY_SERVER"],
    "username": os.environ["PROXY_USER"],
    "password": os.environ["PROXY_PASS"],
}

with sync_playwright() as pw:
    browser = pw.chromium.launch()
    try:
        context = browser.new_context(
            proxy=proxy,
            locale="de-DE",
            timezone_id="Europe/Berlin",
        )
        # open the page from this context to get its settings
        page = context.new_page()
        page.goto("https://httpbin.dev/headers")
        headers = json.loads(page.inner_text("body"))["headers"]
        print("User-Agent:", headers["User-Agent"])
        print("Accept-Language:", headers.get("Accept-Language"))
        print("webdriver:", page.evaluate("navigator.webdriver"))
        print("languages:", page.evaluate("navigator.languages"))
        print("timezone:", page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone"))
    finally:
        browser.close()
```



Here is what we got:

```
User-Agent: ['Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36']
Accept-Language: ['de-DE']
webdriver: True
languages: ['de-DE']
timezone: Europe/Berlin
```



All three values now match a German visitor. The `HeadlessChrome` user agent and the webdriver flag were still there, though.

Note that `browser.new_page()` creates its own context. When we opened a page that way next to this context, it stayed on Africa/Cairo. Next, let's take a look at remote browsers.



## How Do Proxies Work with connect\_over\_cdp() and a Remote Browser?

Scrapfly will automatically rotate proxies for us and we can select the country or proxy type we want to use.

For this example, we connected Playwright to a [Cloud Browser](https://scrapfly.io/products/cloud-browser-api) session in Germany. Instead of `launch()`, we use `connect_over_cdp()`:

python

```python
import json
import os

from playwright.sync_api import sync_playwright

# proxy_pool is datacenter (default) or residential
ws_url = (
    "wss://browser.scrapfly.io"
    f"?api_key={os.environ['SCRAPFLY_KEY']}"
    "&proxy_pool=residential&country=de"
)

with sync_playwright() as pw:
    browser = pw.chromium.connect_over_cdp(ws_url)
    try:
        page = browser.contexts[0].new_page()
        page.goto("https://httpbin.dev/anything")
        print("origin:", json.loads(page.inner_text("body"))["origin"])
        print("timezone:", page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone"))
        print("webdriver:", page.evaluate("navigator.webdriver"))
        print("user agent:", page.evaluate("navigator.userAgent"))
    finally:
        browser.close()
```



Here is what we got:

```
origin: 89.56.179.210
timezone: Europe/Berlin
webdriver: False
user agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36
```



On ip-api.com, 89.56.179.210 shows up as SWN Stadtwerke Neumuenster, a German ISP, with both the proxy and hosting flags set to false. This time, the webdriver flag was false and there's no `HeadlessChrome` in the user agent.

We can select the IP address origin country to reach geographically locked content or switch to residential proxies for those hard-to-scrape targets.

Our first try opened a page in a new context, like in the earlier examples, and Cloud Browser answered that it "runs one browser context per session". So, we open pages with `browser.contexts[0].new_page()` instead.

For the other connection parameters, like `session` and `block_images`, see the [Cloud Browser Playwright docs](https://scrapfly.io/docs/cloud-browser-api/playwright) and the [proxy and geo-targeting docs](https://scrapfly.io/docs/cloud-browser-api/proxy).

### When Is the Web Scraping API Enough Instead of Playwright?

If our Playwright script only loads a page and reads its HTML, we might not need a browser on our side at all. The [Web Scraping API](https://scrapfly.io/products/web-scraping-api) can render the page for us.

All we have to do is replace our browser with the ScrapFly client and enable the [Unblocker](https://scrapfly.io/products/unblocker) (previously ASP) with the `unblocker` argument:

shell

```shell
pip install scrapfly-sdk
```



python

```python
import os

from scrapfly import ScrapeConfig, ScrapflyClient

client = ScrapflyClient(key=os.environ["SCRAPFLY_KEY"])
result = client.scrape(ScrapeConfig(
    url="https://web-scraping.dev/product/1",
    render_js=True,
    unblocker=True,  # older alias: asp=True
    country="DE",
))
print("status:", result.status_code)
print("HTML length:", len(result.content))
```



Here is what we got:

```
status: 200
HTML length: 38664
```



The rendered product page came back through a German proxy, and no browser ran on our machine.



## Which Playwright Proxy Setup Should You Use?

The proxy type is the most important aspect when choosing a web scraping proxy provider or creating a proxy pool. In Playwright, all of them go into the same `proxy` dictionary we used throughout this guide.

Datacenter IPs are commercially assigned to proxy services through cloud servers, and they aren't affiliated with internet service providers (ISPs). This web scraping proxy type is often flagged as high-risk (with a high chance of being automated).

On the bright side, datacenter proxies are widely accessible, reliable, and cheap!

Residential IPs are assigned by ISPs and have a lower risk of being flagged, as they are assigned to home networks. Residential IPs make a reliable web scraping proxy as they are used by real humans!

That being said, proxy services with residential IP addresses are much **pricier** than the datacenter ones. With a browser, this is where blocking images with `page.route()` pays off.

Static residential proxies can suit sessions that need a stable exit IP. For public pages that accept datacenter traffic, a datacenter pool can cost less. Neither choice removes the browser fingerprint leaks shown above.

When the website checks the browser itself, we switch to Cloud Browser or the Web Scraping API from the previous section.



## FAQ

Can you set a proxy for a single page in Playwright?No, the proxy is set on the browser or on a context. We can assign proxy IP address per playwright browser or per context basis, and every page opened in that context uses it.







should you use residential or datacenter proxies with Playwright?Start with datacenter proxies if the target accepts them. Residential exits may help when the target rejects datacenter IPs, but they usually cost more and still need browser, retry and session handling. Choose a sticky exit when the workflow needs the same IP across requests.

For options, see our lists of [datacenter proxies](https://scrapfly.io/blog/posts/the-best-datacenter-proxies) and [residential proxy providers](https://scrapfly.io/blog/posts/top-5-residential-proxy-providers).







Can you use free proxies with Playwright?Yes, but the IPs of free proxies are shared across many users. This means that they have low trust scores, and they are likely to get blocked or challenged with CAPTCHAs.







Why does my Playwright proxy work locally but time out on a server?Check that the server can reach the proxy host and port, that its outbound IP is allowlisted if required, and that `PROXY_SERVER`, `PROXY_USER` and `PROXY_PASS` are set in the service environment. Test the credentials from that server with a bounded curl request. Confirm the proxy protocol too: Chromium does not support SOCKS5 password authentication. A timeout alone does not distinguish these causes; our wrong-password test is one example, not a diagnosis of every server timeout.









## Summary

In this guide, we explained how to use proxies with Playwright. We started by exploring different proxy setups, including:

- Setting a proxy for the whole browser or for each context.
- Adding the proxy username and password.
- Rotating proxies through a proxy list.

We've also taken a look at remote browsers like Scrapfly [Cloud Browser](https://scrapfly.io/products/cloud-browser-api), which ran the same Playwright code with a German residential IP. For Selenium, see our guide on [proxies in Selenium](https://scrapfly.io/blog/posts/how-to-use-proxies-with-selenium).



Legal Disclaimer and PrecautionsThis tutorial covers popular web scraping techniques for education. Interacting with public servers requires diligence and respect:

- Do not scrape at rates that could damage the website.
- Do not scrape data that's not available publicly.
- Do not store PII of EU citizens protected by GDPR.
- Do not repurpose *entire* public datasets which can be illegal in some countries.

Scrapfly does not offer legal advice but these are good general rules to follow. For more you should consult a lawyer.

 

   [  Add as a preferred source ](https://google.com/preferences/source?q=scrapfly.io) Table of Contents















 

  Table of Contents

- [Key Takeaways](#key-takeaways)
- [How Do You Set a Proxy in Playwright?](#how-do-you-set-a-proxy-in-playwright)
- [How Do You Check That Playwright Is Using the Proxy?](#how-do-you-check-that-playwright-is-using-the-proxy)
- [How Do You Authenticate a Proxy in Playwright?](#how-do-you-authenticate-a-proxy-in-playwright)
- [Why Does page.goto() Time Out Instead of Showing a 407?](#why-does-page-goto-time-out-instead-of-showing-a-407)
- [How Do You Use a Different Proxy per Browser Context?](#how-do-you-use-a-different-proxy-per-browser-context)
- [How Do You Rotate Proxies in Playwright?](#how-do-you-rotate-proxies-in-playwright)
- [Rotating Gateway or Proxy List: Which Pattern Fits?](#rotating-gateway-or-proxy-list-which-pattern-fits)
- [When Should a Playwright Session Keep a Sticky IP?](#when-should-a-playwright-session-keep-a-sticky-ip)
- [Using SOCKS5 Proxies with Playwright](#using-socks5-proxies-with-playwright)
- [What Else Goes Through the Proxy, and What Doesn't?](#what-else-goes-through-the-proxy-and-what-doesn-t)
- [Does Playwright Send localhost Traffic Through the Proxy?](#does-playwright-send-localhost-traffic-through-the-proxy)
- [Do Proxy Credentials Reach the Websites You Visit?](#do-proxy-credentials-reach-the-websites-you-visit)
- [How Do You Cut Residential Proxy Bandwidth in Playwright?](#how-do-you-cut-residential-proxy-bandwidth-in-playwright)
- [Why Does Playwright Still Get Blocked Behind a Working Proxy?](#why-does-playwright-still-get-blocked-behind-a-working-proxy)
- [How Do You Match Locale and Timezone to the Proxy Country?](#how-do-you-match-locale-and-timezone-to-the-proxy-country)
- [How Do Proxies Work with connect\_over\_cdp() and a Remote Browser?](#how-do-proxies-work-with-connect-over-cdp-and-a-remote-browser)
- [When Is the Web Scraping API Enough Instead of Playwright?](#when-is-the-web-scraping-api-enough-instead-of-playwright)
- [Which Playwright Proxy Setup Should You Use?](#which-playwright-proxy-setup-should-you-use)
- [FAQ](#faq)
- [Summary](#summary)
 
    Join the Newsletter  Get monthly web scraping insights 

 

  



Scale Your Web Scraping

Anti-bot bypass, browser rendering, and rotating proxies, all in one API. Start with 1,000 free credits.

  No credit card required  1,000 free API credits  Anti-bot bypass included 

 [Start Free](https://scrapfly.io/register) [View Docs](https://scrapfly.io/docs/onboarding) 

 Not ready? Get our newsletter instead. 

 

 ## Related Articles

 [     

 http proxies 

### SOCKS5 vs HTTP Proxy: Key Differences and When to Use Each

Compare HTTP and SOCKS5 proxies for scraping: CONNECT tunnels, DNS resolution, authentication, UDP/QUIC limits, and test...

 

 ](https://scrapfly.io/blog/posts/https-vs-socks-proxies) [     

 python proxies 

### How to Use Proxies with Selenium: Authentication, Rotation, and Residential Proxies

A tested walkthrough of Python Selenium proxies: why Chrome drops credentials, the auth methods that still work, rotatio...

 

 ](https://scrapfly.io/blog/posts/how-to-use-proxies-with-selenium) [     

 proxies 

### 6 Best Datacenter Proxy Providers for Web Scraping in 2026

A comparison of 6 datacenter proxy providers for web scraping in 2026 built from provider-published product facts, cov...

 

 ](https://scrapfly.io/blog/posts/the-best-datacenter-proxies) 

  ## Related Questions

- [ Q What are SOCKS5 proxies and how they compare to HTTP proxies? ](https://scrapfly.io/blog/answers/what-are-socks5-proxies-in-web-scraping)
- [ Q How to block image loading in Selenium? ](https://scrapfly.io/blog/answers/how-to-block-image-loading-in-selenium)
- [ Q How to use proxies with Python httpx? ](https://scrapfly.io/blog/answers/how-to-use-proxies-python-httpx)
- [ Q How to find elements without a specific attribute in BeautifulSoup? ](https://scrapfly.io/blog/answers/how-to-find-elements-without-attribute-in-beautifulsoup)
 
  



   



 Run headless browsers at scale, **1,000 free credits** [Start Free](https://scrapfly.io/register)