Fixing ERR_TUNNEL_CONNECTION_FAILED in Puppeteer

Every proxy mistake we could reproduce, the error each one causes, and how to fix yours.

7 min readTested on Puppeteer 25.12 · Chrome 154
A browser shows ERR_TUNNEL_CONNECTION_FAILED; the gateway answered 403 and the request never reached the home connection or the website.ipinfo.io/jsonThis site can’t be reachedThe proxy refused to open a tunnel.ERR_TUNNEL_CONNECTION_FAILEDCONNECTGatewayreads your username403Home exitWebsitenever reached

In short

The proxy accepted your connection but refused to open the tunnel. With authenticated residential proxies that almost always means the username: a value the gateway can’t read, or filters no exit can match right now.

403 · unreadable username465 · no matching exit

When Puppeteer loads an HTTPS page through a proxy, Chromium doesn’t hand the proxy your request. It can’t: the request is encrypted for the website, not for the proxy. Instead it asks the proxy to open a tunnel, a plain TCP connection to the website that the proxy relays without looking inside. That request is a single line, CONNECT ipinfo.io:443, sent together with your proxy username and password.

The proxy answers before any website is involved. 200 Connection established means the tunnel is open and Chromium starts the TLS handshake with the site. 407 means “send credentials first”, which Chromium does with whatever you passed to page.authenticate(). Any other answer means the proxy refuses, and that is what ERR_TUNNEL_CONNECTION_FAILED reports.

With a residential proxy the refusal almost never comes from the network. A residential gateway does more than forward traffic: it reads your username to decide which home connection to send you through. Country, city, sticky session, all of it is written into the username. If the gateway can’t make sense of it, or can’t find a connection that fits, it refuses the tunnel.

  1. Your script
  2. 403Gateway
  3. Home exit
  4. Website
CONNECT ipinfo.io:443 HTTP/1.1
Proxy-Authorization: Basic am9obi1j…
HTTP/1.1 403 Invalid Request
Puppeteer: ERR_TUNNEL_CONNECTION_FAILED

“usa” is not a two-letter country code. The gateway can’t read the username and stops before choosing an exit.

Fig. 1 — The same request with three usernames. Switch between them to see where it stops; the exchange is what our gateway returned.

Here is the frustrating part: Chromium shows you none of this. Whether the gateway answered 403 or 465, Puppeteer reports the same ERR_TUNNEL_CONNECTION_FAILED. The real reason is in the status line, and to see it you have to take the browser out of the picture. That’s the first fix step below.

The username has a value the gateway can’t read

Every provider writes targeting into the username, each in its own way. Ours checks every value and refuses the whole login when one doesn’t fit: a three-letter country code, a city with a space, a lifetime over 24 hours, or a session label sent to the rotating port. It answers 403; it doesn’t guess what you meant.

403john-country-usathree-letter country code
403john-country-de-session-job1-lifetime-10 on :9000session on the rotating port
200john-country-de-session-job1-lifetime-10 on :10000Germany, same IP for 10 minutes

A whole key it doesn’t know is a different story: it is ignored. john-cc-DE, the way some other gateways write a country, loaded the page in our test with an exit in Russia. No error at all, so look at where your exits are when the data looks odd.

The filters match no connection right now

Every filter narrows the pool, and a connection has to satisfy all of them at once. Country alone almost always works. Country, city and network together can match nobody at a given moment. The gateway then answers 465 instead of quietly giving you a different location, so you never get data from the wrong city without knowing it.

City names go in lower case, without spaces or accents. New York with a space is refused outright (that’s the 403 above), and São Paulo has to arrive as saopaulo.

465john-country-de-city-chicagono Chicago in Germany
200john-country-br-city-saopauloSão Paulo, Brazil

Other errors you might see

Not every proxy mistake ends in a tunnel error. We changed one thing per run, same gateway and same target, and noted what page.goto() reported, so you can match the message you see to its cause.

  • A value the gateway can’t read
    ERR_TUNNEL_CONNECTION_FAILEDGateway sent 403. Two-letter country, city written together, sessions on the sticky port.
  • Filters nobody matches
    ERR_TUNNEL_CONNECTION_FAILEDGateway sent 465. Drop the narrowest filter.
  • Credentials in the proxy flag
    ERR_NO_SUPPORTED_PROXIESFlag gets host and port only.
  • No page.authenticate()
    ERR_INVALID_AUTH_CREDENTIALSAuthenticate each page before goto().
  • Wrong password
    Page loads with 407Copy both values again.
  • https:// as proxy scheme
    ERR_PROXY_CONNECTION_FAILEDUse http://, also for HTTPS sites.
  • socks5:// with a password
    ERR_SOCKS_CONNECTION_FAILEDChromium can’t; use the HTTP port.
  • Wrong port
    ERR_PROXY_CONNECTION_FAILEDCopy host and port from the dashboard.
  • http:// to the SOCKS5 port
    ERR_RESPONSE_HEADERS_TRUNCATEDUse the HTTP port, 9000 or 10000.
  • Another gateway’s key (-cc-DE)
    Page loads, wrong countryUnknown keys are ignored. Write -country-de.
  • No scheme at all
    WorksChromium assumes http://.

Puppeteer 25.12.0, Chrome 154, via.proxies.vision, October 14, 2026. The blue cards are the two causes above.

  1. 1.Test the same login with curl

    curl prints the proxy’s status line, which Chromium hides.

    bash
    curl -sv -x http://via.proxies.vision:9000 \
      --proxy-user "USERNAME-country-de:PASSWORD" \
      https://ipinfo.io/json 2>&1 | grep "< HTTP"
    200 → proxy is fine403 / 465 → username407 → credentials
  2. 2.Strip the username to the bare login

    Remove every targeting parameter and try again. If that works, the parameters are the problem. Copied -cc-US or -s-abc from another provider? Here it’s -country-us and -session-abc, and a key the gateway doesn’t know is ignored rather than refused, so check the exit’s country too.

  3. 3.Add filters back one at a time

    An exit has to match every filter at once. Country alone almost always works; country, city and network together may match nothing right now. City names go in lower case without spaces or accents: newyork, saopaulo.

  4. 4.Check the Puppeteer side

    Credentials only through page.authenticate(), called on every page before goto(). Scheme http://. Host and port exactly as in your dashboard (sessions need the sticky port), and bandwidth left on your plan.

A setup that works

A new IP per identity in a fixed country, and a sticky session when several pages need the same IP. Ran unchanged on October 14, 2026.

proxy.js
const puppeteer = require("puppeteer");

const USER = process.env.PROXY_USER; // from your dashboard
const PASS = process.env.PROXY_PASS;

// Chromium remembers a proxy login per browser context, so one identity
// (one exit IP) per context. The sticky port pins the exit to the label.
async function newIdentity(browser, targeting = "", session = Math.random().toString(36).slice(2, 10)) {
  const context = await browser.createBrowserContext({ proxyServer: "http://via.proxies.vision:10000" });
  const page = await context.newPage();
  // Authenticate every page before its first navigation; no credentials in the proxy URL.
  await page.authenticate({ username: `${USER}${targeting}-session-${session}-lifetime-10`, password: PASS });
  return page;
}

(async () => {
  const browser = await puppeteer.launch();

  // Germany, a new IP for every identity
  const page = await newIdentity(browser, "-country-de");
  await page.goto("https://ipinfo.io/json");
  console.log(await page.evaluate(() => document.body.innerText));

  // The same IP for 10 minutes: the same session label, even in a new context
  const login = await newIdentity(browser, "-country-de", "checkout1");
  await login.goto("https://example.com/login");

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

Building the targeting part by hand is where most of these errors start. The proxy generator in the dashboard writes valid usernames for you.

Still stuck? Check your setup

Paste what you pass to Puppeteer and the checker points at the mistake, using the rules from our test runs. It runs in your browser; nothing is sent.

Check your Puppeteer setup

runs in your browser · nothing is sent

  • Username and password are inside --proxy-server→ ERR_NO_SUPPORTED_PROXIES

    Chromium refuses credentials in the flag. Keep only scheme, host and port there and call page.authenticate({ username, password }) before page.goto().

  • Username is valid (1 filter)

    If you still see ERR_TUNNEL_CONNECTION_FAILED, test the same login with curl (below) and check that your plan has bandwidth left.

Can I put the proxy username and password into ‑‑proxy-server?
No. Chromium rejects a ‑‑proxy-server value that contains credentials and every navigation fails with net::ERR_NO_SUPPORTED_PROXIES. Pass only scheme, host and port in the flag and call page.authenticate() with the username and password.
Does Puppeteer support SOCKS5 proxies with a password?
No. Chromium has no SOCKS5 username/password support, so the connection fails with net::ERR_SOCKS_CONNECTION_FAILED. Use the provider's HTTP port with the same credentials; on via.proxies.vision that is 9000, or 10000 for sticky sessions.
What is the difference between ERR_TUNNEL_CONNECTION_FAILED and ERR_INVALID_AUTH_CREDENTIALS?
ERR_INVALID_AUTH_CREDENTIALS means the proxy asked for credentials and Chromium had none to send: page.authenticate() wasn't called. ERR_TUNNEL_CONNECTION_FAILED means the proxy answered the CONNECT request with something other than 200 or 407, for example 403 for a username it can't read or 465 when no exit matches your filters.
Why does my proxy work in curl but not in Puppeteer?
Usually one of three things: the credentials are in the ‑‑proxy-server flag (Chromium ignores them), page.authenticate() is called after page.goto(), or a new page was opened without its own authenticate() call. Authentication is per page, and Chromium reuses a proxy login within a browser context.

Residential proxies, pay per GB

HTTP and SOCKS5. Country, city and sticky sessions in the username. No subscription.