Proxy error · TUNNEL_CONNECTION_FAILED

Tunnel connection failed

Chrome reached the proxy and asked it to open a tunnel to the site. The proxy answered with something other than 200, so Chrome threw the answer away and showed ERR_TUNNEL_CONNECTION_FAILED. The proxy did say why. You need a tool that prints it.
Whose fault is it?

Whoever the status points at, so read it first: 407 is your login, 403 is what you asked to reach, 502 is the path to the site.

Usually from
The proxy’s answer to CONNECT
Try first
Send the same CONNECT with curl -v and read the status
With a proxy in the path

What a failed tunnel means through a proxy

For an HTTPS site the browser sends CONNECT example.com:443 and waits for 200. Chromium turns every other answer, bar a 407 it can log in to, into net error -111, ERR_TUNNEL_CONNECTION_FAILED. It ignores the proxy’s response body on purpose, so a proxy cannot pass off its own page as the site’s.

That one code covers several different answers. A 407 nobody answered: when Chromium has no credentials to send, or the login prompt is cancelled, it ends with this error rather than show the proxy’s page. A 403 for a port or host the proxy will not tunnel to. A 502 or 503 when the proxy could not reach the site.

Other tools print the status. curl says CONNECT tunnel failed, response 407. Node’s undici says Proxy response (407) !== 200 when HTTP Tunneling. Python puts Tunnel connection failed: 407 Proxy Authentication Required inside a ProxyError.

The key question

Your credentials, the proxy, or the target?

Whoever the status points at, so read it first: 407 is your login, 403 is what you asked to reach, 502 is the path to the site.

  • Your credentials

    The usual cause in browser automation: credentials written into --proxy-server, where Chrome ignores them, or a login handler set up after the first navigation.

  • The proxy

    A 502 or 503 on the CONNECT means the gateway could not reach the target through the exit it picked. A retry often leaves from another exit.

  • The target site

    Indirectly. A target that is down or refuses the exit produces a 502 on the CONNECT, which Chrome reports as this error.

How to tell

  • With curl -v, the line straight after > CONNECT is the proxy’s status: < HTTP/1.1 407, 403 or 502. That is the line Chrome hid.
  • Load an http:// page through the same proxy. It needs no tunnel, so the proxy’s answer comes back as an ordinary response, reason and all.
  • It fails on every site: the login or the proxy. It fails on one site: that host, or the port in its URL.
  • The same credentials work in curl while Puppeteer or Selenium fails: the browser is not sending them with the CONNECT.
Cheapest first

Fixes, in the order to try them

  1. Read the real status with curl

    Send the same CONNECT with curl -v through the same proxy. The status after CONNECT is the answer Chrome discarded, and every other fix depends on it.

    Costs nothing
  2. On a 407, pass credentials the way the tool wants

    Chrome ignores USER:PASS written into --proxy-server. Use the username and password fields in Playwright, page.authenticate in Puppeteer before the first goto, or an extension in Selenium.

    Costs nothing
  3. On a 403, check the port and the host

    Many proxies only tunnel to standard HTTPS ports and answer anything else with 403. Check the port in the URL you are opening, and that the host is one the acceptable use policy allows.

    Costs nothing
  4. On a 502 or 503, retry once and test the site

    A retry on the rotating gateway leaves through a different exit. Open the site without the proxy as well; if it is down for everyone, nothing in your setup will help.

    Costs nothing
  5. Update the allowlist if you use IP auth

    With IP allowlisting, a machine whose public IP has changed is a stranger to the proxy and gets a 407. Add the new address in the dashboard, or switch that machine to username and password.

    Costs some time
Per tool

See the real status and headers

Each sample prints the status the proxy sent back on CONNECT, which is the part Chromium leaves out.

curl
terminal
# the proxy's answer to CONNECT, which Chrome does not show
curl -sv -o /dev/null -x http://USER:[email protected]:8000 https://example.com/ 2>&1 \
  | grep -E '^> CONNECT|^< HTTP|CONNECT tunnel failed'

# > CONNECT example.com:443 HTTP/1.1
# < HTTP/1.1 407 Proxy Authentication Required
# * CONNECT tunnel failed, response 407

The exit code for a refused CONNECT differs between curl releases. The status line does not.

Python Requests
tunnel.py
import requests

PROXY = "http://USER:[email protected]:8000"

try:
    r = requests.get("https://example.com/", proxies={"http": PROXY, "https": PROXY}, timeout=30)
    print("tunnel open, the site answered", r.status_code)
except requests.exceptions.ProxyError as err:
    inner = err.args[0].reason.original_error
    print(inner)

# Tunnel connection failed: 407 Proxy Authentication Required
Scrapy
spiders/tunnel.py
import scrapy
from scrapy.core.downloader.handlers.http11 import TunnelError


class TunnelSpider(scrapy.Spider):
    name = "tunnel"
    start_urls = ["https://example.com/"]

    async def start(self):
        for url in self.start_urls:
            yield scrapy.Request(url, meta={"proxy": "http://USER:[email protected]:8000"}, errback=self.on_error)

    def on_error(self, failure):
        if failure.check(TunnelError):
            self.logger.error("%s", failure.value)

# Could not open CONNECT tunnel with proxy resi.proxymonkey.io:8000 [{'status': 407, 'reason': b'Proxy Authentication Required'}]

A TunnelError is on Scrapy’s default retry list, so with RETRY_TIMES = 2 a refused CONNECT is tried twice more before the errback sees it.

Playwright
tunnel.py
from playwright.sync_api import Error, sync_playwright

PROXY = {"server": "http://resi.proxymonkey.io:8000", "username": "USER", "password": "PASS"}

with sync_playwright() as p:
    browser = p.chromium.launch(proxy=PROXY)
    page = browser.new_page()
    try:
        page.goto("https://example.com/")
    except Error as err:
        print("https:", err.message.splitlines()[0])
        response = page.goto("http://example.com/")
        print("http, no tunnel:", response.status, response.status_text)
    browser.close()

The http:// request needs no tunnel, so whatever status the proxy sends comes back as a response you can read.

Node.js
tunnel.mjs
import { fetch, ProxyAgent } from "undici";

const dispatcher = new ProxyAgent("http://USER:[email protected]:8000");

try {
  const res = await fetch("https://example.com/", { dispatcher });
  console.log("tunnel open, the site answered", res.status);
} catch (err) {
  let e = err;
  while (e && !/Proxy response/.test(e.message ?? "")) e = e.cause;
  console.error(e ? e.message : err);
}

// Proxy response (403) !== 200 when HTTP Tunneling

The samples use the residential gateway, resi.proxymonkey.io:8000. For an ISP or datacenter IP, use USER:PASS@IP:PORT for the address you rented. Your dashboard lists the host and port for every order, and where it differs from this page, the dashboard is right.

Before you buy anything

Will a different proxy line fix it?

When switching helps

Only for scattered 502s on one target from rotating residential exits. A static ISP or datacenter IP has a steadier path to it, if the target accepts that kind of address.

When it will not

A 407 or 403 on the CONNECT follows you to every line. It is about the login or what you asked to reach, not the exit.

The meter

Is a failed request billed?

We bill for request bytes and response bytes, including headers and protocol overhead on the tunnelled connection. Connections that fail before transferring data are not billed. Retries that you initiate are billed and appear as separate entries in your usage log.

From the metering section of our terms of service.

The terms bill request and response bytes and say connections that fail before transferring data are not billed. They do not say which side of that line a refused CONNECT falls on. Look for the rows in your usage log, and ask in Discord if you think they should not be there.

Residential is billed per GB of that traffic. ISP and datacenter addresses are charged per IP for their term, and where a plan includes a traffic allowance, traffic past it is billed per GB under the same rule. The usage log in your dashboard has one row per request with bytes in, bytes out and cost, so you can look up the failed request yourself.

TUNNEL_CONNECTION_FAILED, asked often

Questions people ask about a failed tunnel

Why does Chrome not show the proxy’s error message?

On purpose. A proxy answering CONNECT could otherwise show you a page that pretends to be the site. Chromium discards anything but a 200, or a 407 it can answer, so you need curl or your tool’s exception text to see the real status.

Is ERR_TUNNEL_CONNECTION_FAILED always a password problem?

No, though in Puppeteer and Selenium it often is. It covers every refused CONNECT: a login the browser never sent, a blocked port or host, and a proxy that could not reach the site. The status in curl tells you which.

Why do I get it in Puppeteer but not in curl?

Chrome ignores a username and password written into --proxy-server, so it sends the CONNECT without them. Call page.authenticate before the first navigation, or use a tool that takes proxy credentials as separate fields.

Does it mean the site blocked me?

Not directly. A site that blocks you usually answers inside the tunnel, with a 403 or a challenge page you can read. This error means no tunnel opened at all, so the answer came from the proxy.

The community layer

Still stuck on a failed tunnel?

Paste your error in the Discord: the full message plus the command or the few lines that set up the proxy, with the password taken out. Someone there has seen it before.

Join the Discord

4,200+monkeys in the Discord

  • Help from humans

    Post your error, get an answer. Usually in minutes, usually from someone who has hit the same wall.

  • A status bot that tells on us

    Pool health, incidents and maintenance posted automatically. Including the bad days.

  • Deals and free traffic

    Bonus GB drops, early access to new pools, and the occasional giveaway for a good bug report.

Join the Discord4,200+ monkeys, free to lurk