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

 by [Mohab Yousry](https://scrapfly.io/blog/author/mohab-yousry-9396552a) Nov 12, 2026 29 min read [\#nodejs](https://scrapfly.io/blog/tag/nodejs) [\#proxies](https://scrapfly.io/blog/tag/proxies) [\#puppeteer](https://scrapfly.io/blog/tag/puppeteer) 

 [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-puppeteer "Share on LinkedIn") [  ](https://x.com/intent/tweet?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-puppeteer&text=How%20to%20Use%20Proxies%20with%20Puppeteer%3A%20Authentication%2C%20Rotation%2C%20and%20Per-Page%20Proxies "Share on X") [  ](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-use-proxies-with-puppeteer "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-puppeteer) [  ](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-puppeteer) [  ](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-puppeteer) [  ](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-puppeteer) [  ](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-puppeteer) 



         

  **Browser API**Automate browsers in the cloud with full JavaScript rendering support.

 

 [ Learn More  ](https://scrapfly.io/products/cloud-browser-api) [  Docs ](https://scrapfly.io/docs/scrape-api/javascript-rendering) 

 

 

Puppeteer doesn't make proxy use very obvious. There's no proxy option in `puppeteer.launch()`, and if we add the proxy username and password to the proxy URL, `page.goto()` fails with `net::ERR_NO_SUPPORTED_PROXIES`.

In this tutorial, we'll take a look at how to use proxies with Puppeteer the right way. We'll cover proxy authentication, using different proxies per page, proxy rotation and SOCKS5 proxies. We'll also go over the common proxy errors and what they mean. Let's dive in!



## Key Takeaways

Here's what we found while testing proxies in Puppeteer for this article:

- Puppeteer doesn't come with a proxy option, so the proxy is passed to Chrome using the `--proxy-server` flag. Unfortunately, putting the credentials in that flag resulted in a `net::ERR_NO_SUPPORTED_PROXIES` error.
- Proxy authentication only worked with `page.authenticate()`. A wrong password didn't raise any exception, and we just got a 407 response back.
- Websites asking for Basic auth will also receive our proxy credentials from `page.authenticate()`.
- Browser contexts can each have their own proxy, which allowed us to use multiple proxies in a single browser.
- Chrome doesn't support SOCKS5 proxies with authentication, though the `proxy-chain` package can be used as a workaround.
- The proxy only changed our IP address. The browser still had HeadlessChrome in its user agent and kept our local timezone.

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







## How Do You Set a Proxy in Puppeteer with --proxy-server?

We pass `--proxy-server=scheme://host:port` in the `args` list of `puppeteer.launch()`. Every page in that browser uses this proxy, unless a browser context sets its own.

Puppeteer's `LaunchOptions` has no proxy field, so the proxy is a regular Chrome flag. The value takes `http://`, `https://` or `socks5://`, and Chromium also lists `socks4://`.

Puppeteer can be installed through npm, and it downloads a matching Chrome build during the install. For the rest of the Puppeteer API, see our [Puppeteer scraping basics](https://scrapfly.io/blog/posts/web-scraping-with-puppeteer-and-nodejs) guide.

shell

```shell
npm install puppeteer
```



To start, we 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:



javascript

```javascript
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  args: [`--proxy-server=${process.env.PROXY_SERVER}`],
});

try {
  const page = await browser.newPage();
  const response = await page.goto('https://httpbin.dev/anything');
  const data = await response.json();
  console.log('origin:', data.origin);
} finally {
  await 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. So, if the `origin` is different from the IP address we get without the proxy, the proxy is applied.

Note that a lot of Puppeteer proxy examples online use the `httpbin.dev/ip` endpoint for this check. Unfortunately, this endpoint returned a 404 status code when we tested it in October 2026, which breaks these examples. The `/anything` endpoint is a good replacement, as it returns the same `origin` field.

Alternatively, we can check the proxy IP address manually using our [IP info tool](https://scrapfly.io/web-scraping-tools/ip-info).

Another common pitfall is testing proxies against local web servers. Chrome doesn't use the proxy for `localhost`, `*.localhost`, `127.0.0.1/8`, `[::1]` and link-local addresses. These are called implicit bypass rules in the [Chromium proxy docs](https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/proxy.md).

We ran into this ourselves while testing. Our test page was served from `127.0.0.1`, and it loaded just fine with proxy enabled. However, when we looked at the proxy logs, they were empty! So, a script tested against a local server can look like it works while the proxy isn't doing anything.

To fix this, Chrome has the `--proxy-bypass-list` flag. Passing `<-loopback>` to it removes the implicit rules, and local requests go through the proxy like everything else. It's also handy for the reverse case, like keeping internal company hosts away from the proxy:

javascript

```javascript
const browser = await puppeteer.launch({
  args: [
    `--proxy-server=${process.env.PROXY_SERVER}`,
    // these hosts connect directly
    '--proxy-bypass-list=*.internal.lan;10.0.0.0/8',
  ],
});
```



Entries in this list can be separated by semicolons or commas. We tried both, and Chrome accepted each of them. For more details on proxy types, see our [introduction to proxies in web scraping](https://scrapfly.io/blog/posts/introduction-to-proxies-in-web-scraping) article.

Now that we can set a basic proxy, let's take a look at proxies that require a username and password, as the `--proxy-server` flag doesn't accept them.



## How Do You Authenticate a Proxy with page.authenticate()?

Proxies used for web scraping usually require a username and password to connect. Unfortunately, the `--proxy-server` flag has no place for them, so puppeteer handles proxy credentials differently. We pass only the host and port to the flag, and then call `page.authenticate()` with the credentials on each page we open.

Calling it for every page gets repetitive quickly, so let's put it in a small helper function:



javascript

```javascript
import puppeteer from 'puppeteer';

async function newProxiedPage(browser) {
  const page = await browser.newPage();
  await page.authenticate({
    username: process.env.PROXY_USER,
    password: process.env.PROXY_PASS,
  });
  return page;
}

const browser = await puppeteer.launch({
  args: [`--proxy-server=${process.env.PROXY_SERVER}`],
});

try {
  const page = await newProxiedPage(browser);
  const response = await page.goto('https://httpbin.dev/anything');

  // A rejected password comes back as a 407 response, not an exception
  if (response.status() !== 200) {
    throw new Error(`Proxy returned status ${response.status()}`);
  }

  const body = await page.$eval('body', (el) => el.innerText);
  console.log('origin:', JSON.parse(body).origin);
} finally {
  await browser.close();
}
```



Here's the output:

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



You might wonder why we check the response status at all. It turns out Puppeteer doesn't raise an error when the proxy password is wrong, and we'll get to that shortly!



### Why Do Credentials Inside --proxy-server Fail?

A common mistake is adding the proxy credentials to the `--proxy-server` URL, the same way cURL accepts them as `username:password@host:port`. Unfortunately, Chrome ignores this format. The Chromium docs mention that it "will not use any credentials embedded in the proxy settings."

Let's see what happens when we do it anyway. We added `user:pass@` before the proxy host and loaded httpbin.dev, which gave us this error for both HTTP and HTTPS URLs:

```
Error: net::ERR_NO_SUPPORTED_PROXIES at https://httpbin.dev/anything
```



Interestingly, nothing showed up in the proxy logs. So, the requests didn't fail with a 407 status, as Chrome never reached the proxy at all. The same error happens when the credentials are added to the `proxyServer` option of a browser context.

What about setting the `Proxy-Authorization` header manually through `page.setExtraHTTPHeaders()`? We gave it a try, but the navigation failed with a `net::ERR_INVALID_ARGUMENT` error on HTTP and HTTPS URLs alike.

Lastly, a proxy that requires credentials won't let the page through without `page.authenticate()`. In this case, `goto()` fails with the `net::ERR_INVALID_AUTH_CREDENTIALS` error.

So, `page.authenticate()` is the way to go for proxy credentials. Next, let's take a closer look at how it works.



### What Does page.authenticate() Actually Cover?

Behind the scenes, `page.authenticate()` enables request interception for all requests on the page. The [Puppeteer docs](https://pptr.dev/api/puppeteer.page.authenticate) mention this too, and warn that it "might affect performance". With interception on, Puppeteer replies to the first authentication challenge of every request using our credentials. To disable it again, we can pass `null` to `page.authenticate()`.

The call should happen before `goto()`. However, if a page already failed to load, it's not too late. We called `page.authenticate()` after the failed navigation, retried, and the page loaded fine.

Now, let's get back to the wrong password case from earlier. Instead of throwing an error, `goto()` returns a regular response with the 407 status:

```
status: 407
```



For HTTPS pages, the browser shows Chrome's "HTTP ERROR 407" screen. This is why checking the status code is important, or the scraper will happily keep scraping the error page!

However, the biggest issue with `page.authenticate()` is that it doesn't only answer the proxy. Puppeteer doesn't check where the challenge came from, so a 401 from any website gets the same credentials as a 407 from the proxy.

To confirm this, we loaded the httpbin.dev Basic auth endpoint through our proxy. On the second navigation, after Chrome had cached the proxy login, the website's 401 challenge received our proxy username and password! The same credentials were also sent to two local origins,`127.0.0.1` and `localhost`, without any proxy involved.

So, if a target sends Basic auth challenges, we keep the proxy credentials out of the page using the forwarder from the SOCKS5 section below.

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



## How Do You Set a Different Proxy per Page in Puppeteer?

So far, we've set the proxy for the whole browser. However, Puppeteer pages can't have a proxy of their own, and the smallest unit that takes one is the browser context. So, to use a different proxy per page, we open each page in its own context using `browser.createBrowserContext({ proxyServer })`.

Per-page proxies were a long-awaited Puppeteer feature. They were first requested in [issue #678](https://github.com/puppeteer/puppeteer/issues/678) in September 2017, and browser contexts got the `proxyServer` and `proxyBypassList` options in Puppeteer 10.4.0, released in September 2021.

Note that older Puppeteer examples create contexts using the `createIncognitoBrowserContext()` method. It was renamed to `createBrowserContext()` in Puppeteer 22.0.0 (February 2024), and using it on Puppeteer 25 raises an error:

```
TypeError: browser.createIncognitoBrowserContext is not a function
```



Web scrapers often distribute their traffic through multiple profiles, each with its own proxy, cookies and settings. In Puppeteer, these profiles can be browser contexts. As per the [createBrowserContext()](https://pptr.dev/api/puppeteer.browser.createbrowsercontext) docs, each new context "won't share cookies/cache with other browser contexts" , so each proxy can run its own account or session.

Let's create two contexts with two different proxies. The proxy credentials still go to `page.authenticate()`, as `proxyServer` doesn't accept them:



javascript

```javascript
import puppeteer from 'puppeteer';

const proxies = [
  { server: process.env.PROXY_A, username: process.env.PROXY_A_USER, password: process.env.PROXY_A_PASS },
  { server: process.env.PROXY_B, username: process.env.PROXY_B_USER, password: process.env.PROXY_B_PASS },
];

const browser = await puppeteer.launch();
const contexts = [];

try {
  const results = await Promise.all(
    proxies.map(async ({ server, username, password }) => {
      const context = await browser.createBrowserContext({ proxyServer: server });
      contexts.push(context);

      const page = await context.newPage();
      await page.authenticate({ username, password });
      await page.goto('https://httpbin.dev/anything');

      const body = await page.$eval('body', (el) => el.innerText);
      return { proxy: server, origin: JSON.parse(body).origin };
    }),
  );
  for (const { proxy, origin } of results) console.log(`${proxy} -> ${origin}`);
} finally {
  await Promise.all(contexts.map((context) => context.close()));
  await browser.close();
}
```



Here is what we got:

```
http://127.0.0.1:8000 -> xxx.xxx.xxx.xxx
http://127.0.0.1:8001 -> xxx.xxx.xxx.xxx
```



We masked the IPs again, as both proxies were running on our machine. Each proxy only logged the requests of its own context, with its own username.

### Why Avoid Per-Request Plugins Like puppeteer-page-proxy?

Before browser contexts supported proxies, a common workaround was a per-request plugin like puppeteer-page-proxy. However, these plugins don't route the browser through the proxy. Instead, they enable request interception and resend every request from Node.

This is mentioned in the plugins' own docs. The [puppeteer-page-proxy](https://github.com/Cuadrix/puppeteer-page-proxy) README states that requests "normally made by the browser, are thus made by Node" , and the package depends on the `got` HTTP client. Similarly, the [puppeteer-proxy](https://github.com/gajus/puppeteer-proxy) README says the "browser never makes outbound HTTP requests."

Unfortunately, this makes the scraper easier to detect. The website sees the TLS and HTTP fingerprint of Node instead of Chrome, and a single page can reach it from several IP addresses. For more details on how this is detected, see our [JA3/JA4 TLS fingerprinting guide](https://scrapfly.io/blog/posts/ja3-ja4-tls-fingerprinting-guide-to-detection-and-evasion).

This issue shows up in real scrapers too. A user on [r/webscraping](https://www.reddit.com/r/webscraping/comments/1fn6doi/how_to_apply_proxy_per_page_in_puppeteer_or/) reported that their per-page plugin "somehow triggers recaptcha, while if I directly use the ip that I had used as proxy there is no such trigger."

Moreover, puppeteer-page-proxy hasn't been updated in a while, as its last npm release is 1.3.0 from 3 November 2022. If your scraper doesn't need a browser at all, see our answer on [proxies with Node.js axios](https://scrapfly.io/blog/answers/how-to-use-proxies-nodejs-axios) instead.



## How Do You Rotate Proxies In Puppeteer

Proxy rotation is important for avoiding blocks, as websites track how many requests come from each IP address. In other words, if one proxy loads 50 pages in 5 seconds it's very likely to get throttled or blocked.

Puppeteer sets the proxy when the browser context is created, and it stays the same until the context is closed. So, rotating proxies in Puppeteer comes down to opening a new context with a new proxy, which we can get from a rotating gateway or pick from our own proxy list. For weighting proxies and tracking their health, see our

[How to Rotate Proxies in Web ScrapingIn this article we explore proxy rotation. How does it affect web scraping success and blocking rates and how can we smartly distribute our traffic through a pool of proxies for the best results.](https://scrapfly.io/blog/posts/how-to-rotate-proxies-in-web-scraping)

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

When it comes to rotating gateways where we connect to the same proxy address every time and the provider takes care of switching the IP. If that's the case, our code with `page.authenticate()` from the previous section will do the job as it is. However, we didn't use one for this article, so let's see how to rotate through our own proxy list instead.

We used three proxies in our list and shut down the one on port 8896 on purpose:



javascript

```javascript
import puppeteer from 'puppeteer';

// PROXY_LIST="http://user:pass@host1:port,http://user:pass@host2:port"
const proxies = process.env.PROXY_LIST.split(',').map((entry) => {
  const url = new URL(entry);
  return {
    server: `${url.protocol}//${url.host}`,
    username: decodeURIComponent(url.username),
    password: decodeURIComponent(url.password),
  };
});

const badProxies = new Set();
let nextIndex = 0;

async function scrapeWithRotation(browser, url) {
  for (let attempt = 0; attempt < proxies.length; attempt++) {
    const proxy = proxies[(nextIndex + attempt) % proxies.length];
    if (badProxies.has(proxy.server)) continue;

    // Credentials never go into proxyServer, only host and port
    const context = await browser.createBrowserContext({ proxyServer: proxy.server });
    try {
      const page = await context.newPage();
      await page.authenticate({ username: proxy.username, password: proxy.password });
      const response = await page.goto(url, { timeout: 30_000 });

      if (!response.ok()) throw new Error(`status ${response.status()}`);

      nextIndex = (nextIndex + attempt + 1) % proxies.length;
      const title = await page.$eval('h3.product-title', (el) => el.textContent.trim());
      return { url, proxy: proxy.server, title };
    } catch (error) {
      console.warn(`${proxy.server} failed: ${error.message.split('\n')[0]}`);
      badProxies.add(proxy.server);
    } finally {
      await context.close();
    }
  }
  throw new Error(`All proxies failed for ${url}`);
}

const browser = await puppeteer.launch();
try {
  for (let id = 1; id <= 3; id++) {
    const result = await scrapeWithRotation(browser, `https://web-scraping.dev/product/${id}`);
    console.log(`${result.title} <- ${result.proxy}`);
  }
} finally {
  await browser.close();
}
```



```
http://127.0.0.1:8001 failed: status 407
http://127.0.0.1:8896 failed: net::ERR_PROXY_CONNECTION_FAILED at https://web-scraping.dev/product/1
Box of Chocolate Candy <- http://127.0.0.1:8000
Dark Red Energy Potion <- http://127.0.0.1:8000
Teal Energy Potion <- http://127.0.0.1:8000
```



Product 1 landed on the dead proxy and failed right away with `net::ERR_PROXY_CONNECTION_FAILED`. Then, it tried port 8000 and the page loaded.



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

However, rotating the IP isn't always what we want. Some websites remember the IP address a session started from. For example, when we log in to web-scraping.dev, it sets a `login_ip` cookie holding our IP address. If the next request comes from a different IP, the session doesn't add up anymore. So, for logins, carts and other multi-step flows, we keep the same IP until the session is finished.

To make things more complicated, rotating gateways can change the IP address within a single page load! This happens because Chrome connects to many hosts when loading a page, and each connection is a new CONNECT tunnel through the gateway. For example, when we loaded one page through our test proxy, Chrome connected to seven different hosts. Only one of these was the target website, while the rest were Google services Chrome uses in the background.

So, if our gateway rotates IPs per connection, one page could be loaded through multiple IP addresses. To prevent this, most proxy providers support sticky sessions, which let us keep the same IP address for a set period of time. Usually, the session is defined in the proxy username, which is then passed to `page.authenticate()` as usual.

Finally, it's best to give each sticky session its own browser context. This way, the session's cookies and IP address stay in sync. With that, let's take a look at SOCKS5 proxies next.



## How Do You Use an Authenticated SOCKS5 Proxy in Puppeteer with proxy-chain?

SOCKS5 is another popular proxy protocol, and Puppeteer supports it out of the box. All we have to do is use the `socks5://` scheme in the `--proxy-server` flag. That being said, most SOCKS5 proxies offered by proxy providers require a username and password, and this is where things get complicated.

To see this in action, let's launch Chrome with a password-protected SOCKS5 proxy and request httpbin.dev:

```
Error: net::ERR_SOCKS_CONNECTION_FAILED at https://httpbin.dev/anything
```



The request failed right away! This is because Chrome doesn't support SOCKS5 authentication at all. According to the Chromium docs, "No authentication methods are supported for SOCKSv5 in Chrome." When connecting to our server, Chrome only offered to connect without a password, so our server simply refused the connection.

What about the authentication methods we used for HTTP proxies? Unfortunately, neither of them works here. The `page.authenticate()` method doesn't apply to SOCKS proxies, and adding the credentials to the proxy URL results in the same `net::ERR_NO_SUPPORTED_PROXIES` error we saw earlier.

This is where the `proxy-chain` package comes in. It can run a local proxy server that forwards our requests to the authenticated SOCKS5 proxy. For this, we'll use its `anonymizeProxy()` function, which takes the SOCKS5 proxy URL with the credentials and returns the address of a local HTTP proxy on `127.0.0.1` that doesn't require any login. Let's install it using npm and give it a shot:

shell

```shell
npm install proxy-chain
```





javascript

```javascript
import puppeteer from 'puppeteer';
import { anonymizeProxy, closeAnonymizedProxy } from 'proxy-chain';

// socks5://user:pass@host:port -> http://127.0.0.1:<random port>
const localProxy = await anonymizeProxy(process.env.SOCKS_URL);

const browser = await puppeteer.launch({
  args: [`--proxy-server=${localProxy}`],
});

try {
  const page = await browser.newPage();
  const response = await page.goto('https://httpbin.dev/anything');
  const data = await response.json();
  console.log('origin:', data.origin);
} finally {
  await browser.close();
  // true also closes the open connections
  await closeAnonymizedProxy(localProxy, true);
}
```



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)Here, the `anonymizeProxy()` call starts the forwarder on a random local port and returns its address, which looks like `http://127.0.0.1:` followed by the port number.

Here is what we got:

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



What's great about `proxy-chain` is that it works with regular HTTP proxies as well. We tried it with our authenticated HTTP proxy, and the pages loaded fine without calling `page.authenticate()`.

This also solves the issue of proxy credentials leaking to websites, which we saw earlier with `page.authenticate()`. The username and password are kept by `proxy-chain`, so Chrome can't send them anywhere!

Just make sure to close each forwarder with `closeAnonymizedProxy()` once you're done, otherwise they'll keep running in the background.

For more details on the protocol, refer to our answer on [SOCKS5 proxies in web scraping](https://scrapfly.io/blog/answers/what-are-socks5-proxies-in-web-scraping). Next, let's put all the `net::ERR_*` errors we've seen in one place.



## What Do Puppeteer Proxy Errors Like ERR\_NO\_SUPPORTED\_PROXIES and 407 Mean?

Proxy issues in Puppeteer usually show up as a `net::ERR_*` error thrown by `page.goto()`. However, there's one important exception we've seen earlier. When the proxy rejects our credentials, `goto()` doesn't throw anything, and we get a regular response with the 407 status code instead.

To make debugging easier, we've collected all of the proxy errors we ran into while writing this guide, using Puppeteer 25.12.0 and Chrome 154:

| What you see | What it means | Fix |
|---|---|---|
| `net::ERR_NO_SUPPORTED_PROXIES` | Chrome found no usable proxy in the setting. In our run, `user:pass@` in the proxy URL caused it | Move credentials to `page.authenticate()` or a `proxy-chain` forwarder |
| `net::ERR_INVALID_AUTH_CREDENTIALS` | The proxy asked for credentials and the page had none | Call `page.authenticate()` on that page before `goto()` |
| Status 407 (no exception) | Credentials were sent and rejected | Fix the username or password and check `response.status()` |
| `net::ERR_PROXY_CONNECTION_FAILED` | Chrome could not connect to the proxy host or port | Check host, port and scheme, and that the proxy is reachable from where the script runs |
| `net::ERR_SOCKS_CONNECTION_FAILED` | The SOCKS handshake failed. In our run, a SOCKS5 proxy that requires a password | Use `anonymizeProxy()` |
| `net::ERR_INVALID_ARGUMENT` | A `Proxy-Authorization` header set through `setExtraHTTPHeaders()` | Remove the header and use `page.authenticate()` |
| `net::ERR_TUNNEL_CONNECTION_FAILED` | The proxy answered CONNECT with an error status instead of opening the tunnel (403, 502 and 503 all produced it in our run) | Send the same URL through the proxy with `curl -v -x` to read the status and message the proxy returned |

Most of these are self-explanatory, except for `net::ERR_TUNNEL_CONNECTION_FAILED`. The [Chromium net error list](https://chromium.googlesource.com/chromium/src/+/HEAD/net/base/net_error_list.h) describes it as "a tunnel connection through the proxy could not be established" , which doesn't tell us much. The actual reason is in the status code the proxy returned, and Chrome doesn't show it to us. So, it's best to send the same request through cURL and read the proxy's response directly.

Another common case is a scraper that works locally but fails once deployed. Remember the implicit bypass rules from the first section? If the local tests ran against `localhost`, Chrome skipped the proxy completely, and the proxy was never really tested!

For more details on the 407 status code, see our

[What is HTTP 407 Status Code and How to Fix itImagine trying to access a website, only to be stopped in your tracks by a frustrating error code. If you've encountered HTTP 407, you know how disruptive it can be, especially when working with proxies that demand seamless connectivity.](https://scrapfly.io/blog/posts/what-is-http-407-status-code-and-how-to-fix-it)



## How Do You Cut Residential Proxy Bandwidth in Puppeteer?

Headless browsers are heavy on bandwidth. Since Chrome renders the full page, it pulls in all of the images, fonts and videos as well, while we only need the HTML for scraping.

Residential proxies are often billed by the GB, so this adds up fast.

We also found that not all of the proxy traffic came from the page. We gave it a try with our test proxy, and besides the website, Chrome connected to `accounts.google.com`, `update.googleapis.com` and several other Google hosts in the background. We can add these to `--proxy-bypass-list` so they don't count against our proxy plan.

For the page resources, we can turn on `page.setRequestInterception(true)` and abort the requests with the image, font or media resource type. Note that the page will see these as failed requests.

Scrapfly [Proxy Saver](https://scrapfly.io/docs/proxy-saver/getting-started) can also handle this on the proxy side, in front of our current proxy provider. For HTTPS pages, Chrome has to trust its [certificate](https://scrapfly.io/docs/proxy-saver/certificates) first.

For more blocking rules, see our answer on how to [block resources in Puppeteer](https://scrapfly.io/blog/answers/how-to-block-resources-in-puppeteer) 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 Puppeteer still gets blocked.



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

A proxy handles IP addresses only of our scraper's identity. Nowdays there are advanced anti-bot services that go deeper, and they can fingerprint the browser itself through JavaScript.

Headless Puppeteer is especially easy to fingerprint as it leaks details about its automation to the JavaScript execution context. The user agent contains `HeadlessChrome` and the `navigator.webdriver` variable is set to `true`, meaning any website can tell the browser is controlled by a program.

Another thing to keep in mind is the browser language and timezone. Puppeteer doesn't get these from the proxy, it uses whatever is set on our own computer.

For example, if we scrape a German website through a German IP, our browser will still say it speaks US English. Those settings can be a fingerprint inconsistency; the next section shows how to align them with the proxy location.

Other than that, there are TLS and browser fingerprinting checks, though these are beyond the scope of this article. See [how anti-bot detection works](https://scrapfly.io/blog/posts/how-anti-bot-detection-works) and our [bypass guides](https://scrapfly.io/bypass) to learn more, or our [Puppeteer Stealth](https://scrapfly.io/blog/posts/puppeteer-stealth-complete-guide) guide for hiding the webdriver flag.

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

When scraping websites in other countries, the browser language should match the proxy location. Otherwise, our scraper stands out from the regular visitors of that website.

If the whole browser uses proxies from one country, the easiest way is to set it at launch. We add the `--accept-lang=de-DE,de` flag and set the `TZ` environment variable, and every page we open gets the same language and timezone:



javascript

```javascript
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  args: [`--proxy-server=${process.env.PROXY_SERVER}`, '--accept-lang=de-DE,de'],
  env: { ...process.env, TZ: 'Europe/Berlin', LANG: 'de_DE.UTF-8' },
});

try {
  const page = await browser.newPage();
  await page.authenticate({
    username: process.env.PROXY_USER,
    password: process.env.PROXY_PASS,
  });
  await page.goto('https://httpbin.dev/headers');

  const body = await page.$eval('body', (el) => el.innerText);
  const headers = JSON.parse(body).headers;
  const browserSide = await page.evaluate(() => ({
    languages: navigator.languages,
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
  }));

  console.log('Accept-Language header:', headers['Accept-Language']);
  console.log('navigator.languages:', browserSide.languages);
  console.log('Timezone:', browserSide.timezone);
} finally {
  await browser.close();
}
```



Here is what we got on Linux:

```
Accept-Language: [ 'de-DE,de;q=0.9' ]
languages: [ 'de-DE', 'de' ]
timezone: Europe/Berlin
```



Great, our browser looks like a German visitor now! Note that the `Intl` locale was still `en-US` at first. We had to add `LANG: 'de_DE.UTF-8'` to `env` as well to change it.

We also tried a few other ways that didn't work as well. For example, `page.setExtraHTTPHeaders()` only changes the `Accept-Language` header, while `navigator.languages` stays `['en-US']`.

The `--lang=de-DE` flag didn't change anything in headless mode. And `page.emulateTimezone()` only works for a single page, so any new page went back to our own timezone.

However, `page.emulateTimezone()` is still useful when each context uses a proxy from a different country. In this case, we call it on every page and set the language with a CDP session through `Emulation.setUserAgentOverride` and its `acceptLanguage` field.

Unfortunately, none of this hides the `HeadlessChrome` user agent or the webdriver flag. Next, let's take a look at remote browsers.



## How Do Proxies Work with puppeteer.connect() and a Remote Browser?

Running Chrome on our own machine works well for small projects. However, at scale, many developers move to remote browsers, and Puppeteer connects to these with `puppeteer.connect()`.

The catch is that a remote browser is already running when we connect to it. `connect()` can't change launch flags, but a remote browser that allows new contexts can still accept `createBrowserContext({ proxyServer })`; Scrapfly Cloud Browser uses the connection parameters shown below.

This is where Scrapfly [Cloud Browser](https://scrapfly.io/products/cloud-browser-api) comes in handy. It manages the proxies on its end, and all we have to do is add the proxy type and country to the connection URL.



javascript

```javascript
import puppeteer from 'puppeteer';

// proxy_pool is datacenter (default) or residential
const ws = `wss://browser.scrapfly.io?api_key=${process.env.SCRAPFLY_KEY}&proxy_pool=residential&country=de`;

const browser = await puppeteer.connect({ browserWSEndpoint: ws });

try {
  const page = await browser.newPage();
  const response = await page.goto('https://httpbin.dev/anything');
  const data = await response.json();
  console.log('origin:', data.origin);
  console.log('Accept-Language:', data.headers['Accept-Language']);
  console.log('languages:', await page.evaluate(() => navigator.languages));
  console.log('timezone:', await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone));
  console.log('webdriver:', await page.evaluate(() => navigator.webdriver));
} finally {
  // closing the browser stops the session
  await browser.close();
}
```



Here is what we got:

```
origin: xxx.xxx.xxx.xxx
Accept-Language: [ 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7' ]
languages: [ 'de-DE', 'de', 'en-US', 'en' ]
timezone: Europe/Berlin
webdriver: false
```



As we can see, the browser language and timezone were set to Germany automatically, so there was no need for the `--accept-lang` and `TZ` tricks from the previous section. Moreover, the `navigator.webdriver` value came back as `false`.

Note that the proxy is assigned once the session starts and can't be changed after that. So, to switch to another country, we have to close the browser and connect again with a different `country` value.

For more details on the available parameters, refer to the [Cloud Browser Puppeteer docs](https://scrapfly.io/docs/cloud-browser-api/puppeteer) and the [proxy and geo-targeting docs](https://scrapfly.io/docs/cloud-browser-api/proxy).

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

Puppeteer is often used just to render javascript pages. In that case, we don't really need a whole browser with its own proxy setup on our machine.

Scrapfly's [Web Scraping API](https://scrapfly.io/products/web-scraping-api) can render the page in the cloud and return the HTML to us. Each request goes out through a different IP from Scrapfly's own proxy pool, so there is no proxy list to maintain on our side.

Let's give it a try. We'll be using the Scrapfly SDK for Node.js, which can be installed using `npm`. Then, we request a product page from web-scraping.dev through a German IP, with the [Unblocker](https://scrapfly.io/products/unblocker) (previously ASP) turned on to handle any blocking:

shell

```shell
npm install scrapfly-sdk
```





javascript

```javascript
import { ScrapflyClient, ScrapeConfig } from 'scrapfly-sdk';

const client = new ScrapflyClient({ key: process.env.SCRAPFLY_KEY });

const result = await client.scrape(
  new ScrapeConfig({
    url: 'https://web-scraping.dev/product/1',
    render_js: true,
    unblocker: true, // older alias: asp
    country: 'DE',
  }),
);

console.log('HTML length:', result.result.content.length);
```



Here is what we got with `scrapfly-sdk` 0.12.0:

```
HTML length: 38708
```



That being said, the Web Scraping API isn't a full replacement for Puppeteer. If our scraper has to log in, fill out forms or click through several pages, we're better off using Puppeteer with the Cloud Browser we covered in the previous section.



## Which Puppeteer Proxy Setup Should You Use?

We went through quite a few proxy setups in this article, and it can be hard to tell which one we actually need. Most scrapers only use one or two of them.

The easiest case is a proxy that allowlists our server IP. Here, there is no password, so the `--proxy-server` flag is all we need.

Things get more involved once we add credentials, multiple accounts or SOCKS5. Each of these adds a bit more code, as we've seen in the previous sections.

Also, it doesn't matter if we're using datacenter or residential proxies. Puppeteer sets them up the same way. Here is a table with all of the setups:

| Your situation | Setup | What changes in your code |
|---|---|---|
| One proxy, IP-allowlisted | `--proxy-server` | Launch arg |
| HTTP proxy with username and password | `--proxy-server` + `page.authenticate()` | One call per page |
| Different IP per tab or account | `createBrowserContext({ proxyServer })` | One context per proxy |
| Rotate through a list | Context per job, next proxy on failure | Retry loop |
| SOCKS5 with credentials, or a library launches Chrome | `proxy-chain` `anonymizeProxy()` | Local forwarder |
| Keep your provider, cut bandwidth | Proxy Saver as `--proxy-server` | Endpoint swap |
| The site blocks the browser, not the IP | Cloud Browser via `puppeteer.connect()` | `launch()` to `connect()` |
| Only need the rendered HTML | Web Scraping API | Replace the browser |

Remember the `HeadlessChrome` user agent and the webdriver flag from earlier? If residential proxies get blocked too, those are the likely reason. So, the last two rows are the ones to try in that case.



## FAQ

Does page.authenticate() send my proxy credentials to other websites?Yes, Puppeteer uses the `page.authenticate()` credentials for every authentication request, meaning a website with basic auth can receive them as well. We can avoid this by using a `proxy-chain` forwarder instead.







Should you use residential or datacenter proxies with Puppeteer?Datacenter proxies are a good fit for teams that can spend time reverse engineering the target websites. Residential proxies need less effort as they have a higher trust score, but they get pricey with a browser loading every image and font.

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 Puppeteer?Yes, but free proxies are public and used by many people at once. This means that their IPs are often already blocked, and whoever runs the proxy can see our traffic too.







Is puppeteer-page-proxy still a good option in 2026?No, browser contexts are the better option now. They did not update the package since 2022 and it uses Node to send requests, which causes the traffic to look different from a real browser.







Do you still need page.authenticate() in Puppeteer 25?Yes, there is no username or password option in the Puppeteer launch or context options. Chrome doesn't accept credentials in the proxy URL either, so it's either `page.authenticate()`, a `proxy-chain` forwarder or an IP-allowlisted proxy.









## Conclusion

In this article, we've taken a look at how to set up proxies in Puppeteer. We started with a basic proxy for the whole browser and then went through more advanced setups, such as:

- Using the `--proxy-server` flag to proxy the whole browser.
- Logging in to the proxy with `page.authenticate()`.
- Giving each browser context its own proxy and rotating through a proxy list.
- Getting authenticated SOCKS5 proxies to work with `proxy-chain`.

We've also seen that a proxy only changes the IP address, and Puppeteer can still get blocked because of the browser itself. For these cases, we tried Scrapfly [Cloud Browser](https://scrapfly.io/products/cloud-browser-api), which ran our Puppeteer code through a German residential IP with only a change from `.launch()` to `connect()`. And for scrapers that only need the HTML, the web scraping API can replace the browser completely.



### Web Scraping API

Scrape any website with our powerful API. Anti-bot bypass, JavaScript rendering, and rotating proxies built-in.



[Try Web Scraping API](https://scrapfly.io/docs/scrape-api/getting-started)



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 Puppeteer with --proxy-server?](#how-do-you-set-a-proxy-in-puppeteer-with-proxy-server)
- [How Do You Authenticate a Proxy with page.authenticate()?](#how-do-you-authenticate-a-proxy-with-page-authenticate)
- [Why Do Credentials Inside --proxy-server Fail?](#why-do-credentials-inside-proxy-server-fail)
- [What Does page.authenticate() Actually Cover?](#what-does-page-authenticate-actually-cover)
- [How Do You Set a Different Proxy per Page in Puppeteer?](#how-do-you-set-a-different-proxy-per-page-in-puppeteer)
- [Why Avoid Per-Request Plugins Like puppeteer-page-proxy?](#why-avoid-per-request-plugins-like-puppeteer-page-proxy)
- [How Do You Rotate Proxies In Puppeteer](#how-do-you-rotate-proxies-in-puppeteer)
- [Rotating Gateway or Proxy List: Which Pattern Fits?](#rotating-gateway-or-proxy-list-which-pattern-fits)
- [When Should a Puppeteer Session Keep a Sticky IP?](#when-should-a-puppeteer-session-keep-a-sticky-ip)
- [How Do You Use an Authenticated SOCKS5 Proxy in Puppeteer with proxy-chain?](#how-do-you-use-an-authenticated-socks5-proxy-in-puppeteer-with-proxy-chain)
- [What Do Puppeteer Proxy Errors Like ERR\_NO\_SUPPORTED\_PROXIES and 407 Mean?](#what-do-puppeteer-proxy-errors-like-err-no-supported-proxies-and-407-mean)
- [How Do You Cut Residential Proxy Bandwidth in Puppeteer?](#how-do-you-cut-residential-proxy-bandwidth-in-puppeteer)
- [Why Does Puppeteer Still Get Blocked Behind a Working Proxy?](#why-does-puppeteer-still-get-blocked-behind-a-working-proxy)
- [How Do You Match Timezone and Language to the Proxy Country?](#how-do-you-match-timezone-and-language-to-the-proxy-country)
- [How Do Proxies Work with puppeteer.connect() and a Remote Browser?](#how-do-proxies-work-with-puppeteer-connect-and-a-remote-browser)
- [When Is the Web Scraping API Enough Instead of Puppeteer?](#when-is-the-web-scraping-api-enough-instead-of-puppeteer)
- [Which Puppeteer Proxy Setup Should You Use?](#which-puppeteer-proxy-setup-should-you-use)
- [FAQ](#faq)
- [Conclusion](#conclusion)
 
    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

 [     

 python playwright 

### How to Use Proxies with Playwright: Authentication, Rotation, and Per-Context Proxies

A tested walkthrough of Playwright proxies covering launch and context options, authentication, rotation, SOCKS5 limits,...

 

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

 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) [     

 python nodejs 

### How to use Headless Chrome Extensions for Web Scraping

In this article, we'll explore different useful Chrome extensions for web scraping. We'll also explain how to install Ch...

 

 ](https://scrapfly.io/blog/posts/how-to-use-browser-extensions-with-playwright-puppeteer-and-selenium) 

  ## Related Questions

- [ Q How to Rotate Proxies in Scrapy ](https://scrapfly.io/blog/answers/scrapy-spiders-proxy-rotation)
- [ 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 Solve the cURL (60) Error When Using Proxy? ](https://scrapfly.io/blog/answers/how-to-solve-the-curl-60-error-when-proxy)
- [ Q How To Copy as cURL With Google Chrome? ](https://scrapfly.io/blog/answers/how-to-copy-as-curl-with-chrome)
 
  



   



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