← all articles

How to fix ERR_TUNNEL_CONNECTION_FAILED in a cloud browser proxy setup

You create a cloud browser session, the vendor hands back a websocket URL, and your script attaches without a complaint. Then the first page.goto("https://...") dies with net::ERR_TUNNEL_CONNECTION_FAILED. So does the next URL. So does every one after it.

That is the case in browser-use issue #4694: the cloud connect succeeds and every navigation fails. The direct answer: a successful connect only proves your script can reach the cloud vendor. The error comes from a different leg, the one between the cloud browser and your proxy, so it is rarely your agent code or framework. It is the proxy’s reply to a CONNECT request, which you can read yourself.

This is for operators running browser agents on a hosted browser with a proxy attached, whether the vendor supplies the proxy or you bring your own. By the end you will know which leg is failing, have fixed the usual causes, and have a short preflight script that catches a bad proxy before an agent spends model calls on it.

What you need

  • a cloud browser session you can attach to over CDP, meaning your vendor gives you a wss:// endpoint. Proxy parameter names differ by vendor and change, so keep their docs open. As of September 2026 I’d trust those over any blog post, this one included.
  • the proxy details: host, port, protocol (http or socks5), username, password, and whether the provider authenticates by credentials or by allowlisting a source IP
  • Python 3.10 or newer, with pip install playwright requests and then playwright install chromium for the local test in step 5
  • curl. Windows 10 ships curl.exe. In Windows PowerShell type curl.exe, because plain curl is an alias for Invoke-WebRequest there.
  • about 30 minutes. There is no extra cost beyond your existing cloud browser and proxy plans, since each test moves a few kilobytes.

Step by step

Step 1: work out which leg the error belongs to

Your setup has two legs. Leg one is your script to the vendor over the CDP websocket. Leg two is the cloud browser to the proxy, then the proxy to the site. ERR_TUNNEL_CONNECTION_FAILED is a leg two error. Chrome asked the proxy for an HTTP CONNECT tunnel to an https site and the proxy did not agree. RFC 9110 defines CONNECT, and a 2xx reply is what opens the tunnel. Leg one problems, like the Chrome 136 CDP refusal, fail before any page loads.

Two lookalikes: ERR_PROXY_CONNECTION_FAILED means the browser never reached the proxy, and ERR_NO_SUPPORTED_PROXIES means it rejected the proxy scheme or auth type.

Expected output: the exact string net::ERR_TUNNEL_CONNECTION_FAILED at https://... in your logs.

If it breaks: a different error name changes the branch. Go to step 6 for the first lookalike and step 5 for the second.

Step 2: split http from https inside the cloud browser

Take the agent out of the picture. No model, no framework, just a short script that attaches over CDP and loads one plain URL and one https URL. Through an http proxy, a plain http:// page is fetched by asking the proxy directly, while https:// needs CONNECT. If only the second fails, the proxy is refusing tunnels.

import os
from playwright.sync_api import sync_playwright

CDP_URL = os.environ["CLOUD_CDP_URL"]  # wss:// endpoint from your vendor

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(CDP_URL)
    context = browser.contexts[0] if browser.contexts else browser.new_context()
    page = context.new_page()
    for url in ("http://example.com", "https://example.com"):
        try:
            resp = page.goto(url, timeout=20000)
            print(url, "->", resp.status)
        except Exception as e:
            print(url, "-> FAIL", str(e).splitlines()[0])

I reuse the vendor’s default context because I can’t promise every vendor applies session settings to new ones.

Expected output:

http://example.com -> 200
https://example.com -> FAIL Page.goto: net::ERR_TUNNEL_CONNECTION_FAILED at https://example.com/

If it breaks: if both load, you have no tunnel problem. What is failing in your agent is the site answering, and why AI browser agents get blocked is the better read. If both fail, the proxy leg is broken wider than CONNECT and step 3 will show why.

Step 3: repeat the CONNECT with curl

Now reproduce the request without a browser. curl prints the CONNECT exchange, which Chrome squashes into one error name. The -x flag sets the proxy, documented on the curl manpage.

# macOS/Linux. On Windows PowerShell use curl.exe and -o NUL
curl -v -x "http://USER:PASS@HOST:PORT" https://example.com -o /dev/null

# socks5 proxy, DNS resolved by the proxy
curl -v -x "socks5h://USER:PASS@HOST:PORT" https://example.com -o /dev/null

Expected output: for an http proxy, a line > CONNECT example.com:443 HTTP/1.1 followed by < HTTP/1.1 200 Connection established (wording varies), then a normal TLS handshake. SOCKS shows its own handshake lines instead.

If it breaks: whatever status follows CONNECT is your answer. Step 4 decodes it.

Step 4: read the status and act on it

Providers vary, but these are the usual ones:

  • 407: credentials missing, wrong, or the plan has lapsed. Some providers pack country or session options into the username, so check its format.
  • 403: the proxy refused this tunnel by policy. Usually a port other than 443, a destination on the provider’s restricted list, a source IP that is not allowlisted, or an account limit. Ask the provider which.
  • 400 or 405: this is not a CONNECT-capable proxy, often the wrong port or an API port.
  • 502, 503 or 504: the tunnel was accepted but the exit could not reach the site. Try another endpoint.
  • 429: too many concurrent connections or sessions on the account.
  • no status, a reset or a timeout: a firewall, the wrong port, or a protocol mismatch such as SOCKS spoken to an http port.

Expected output: one status you can name.

If it breaks: if curl gets a clean 200 from your machine, the proxy works from here. Carry on to step 5.

Step 5: run the same proxy in a local browser

This separates the browser from the proxy. Local Chromium, same proxy, credentials passed as the Playwright proxy docs describe.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(proxy={
        "server": "http://HOST:PORT",
        "username": "USER",
        "password": "PASS",
    })
    page = browser.new_page()
    page.goto("https://example.com")
    print(page.title())
    browser.close()

Expected output: Example Domain.

If it breaks: three combinations tell you where to look.

  • local works, cloud fails: the difference is location, so go to step 6
  • local and curl both fail: the proxy itself is the problem, so take the step 4 status to the provider
  • curl works, local fails: credential handling. As of September 2026 Chrome’s --proxy-server flag takes no credentials, so user:pass@ inside it does not authenticate. Pass username and password as separate fields, or use the equivalent fields in your vendor’s session options.

Step 6: check what is different about the cloud location

Two things change when the browser moves from your laptop to a vendor’s datacentre: its source IP and which networks it can reach.

  • a proxy on 192.168.x.x, 10.x.x.x, 127.0.0.1 or a .local name is invisible from the cloud. Use a public hostname.
  • if your provider authenticates by allowlisted source IP, the vendor’s egress IP is not on the list. Find it by opening a session with no proxy and printing the address.
page.goto("http://api.ipify.org")
print(page.inner_text("body"))

Expected output: an IP address that is not yours. If the vendor’s addresses change from session to session, an allowlist can’t work, so ask the provider for username and password auth.

If it breaks: if the address is already allowlisted and it still fails, go back to the provider with the step 4 status and ask for their CONNECT logs for that timestamp.

Step 7: look for a second proxy setting

A proxy can be set in three places: your agent framework config, the vendor’s session options, and the environment. A Chrome you launch yourself on Linux can pick up http_proxy and https_proxy from the environment. When two settings disagree you debug the wrong one.

env | grep -i proxy
grep -rniE "proxy" .env config/ 2>/dev/null
# PowerShell: Get-ChildItem Env: | Where-Object Name -match 'proxy'

Expected output: one proxy setting you recognise, or none if the vendor sets it for you.

If it breaks: delete the extras, keep one place as the source of truth, and re-run step 2.

Step 8: fix the cause, then add a preflight

Apply the fix from step 4 or 6, re-run step 2, then keep this script and run it before each agent session. I don’t put the model in the loop for this. An agent that retries a failing goto with a model call between attempts is paying tokens for a network fault.

import os, sys, requests

proxy = os.environ["AGENT_PROXY_URL"]  # http://USER:PASS@HOST:PORT

try:
    r = requests.get("https://example.com", proxies={"https": proxy}, timeout=15)
    print("tunnel ok", r.status_code)
except requests.exceptions.ProxyError as e:
    print("tunnel failed:", e)
    sys.exit(1)

Expected output: tunnel ok 200.

If it breaks: the exception text normally carries the CONNECT status, for example Tunnel connection failed: 407 Proxy Authentication Required. Don’t print the proxy URL in your logs, it holds the password.

Common pitfalls

  • testing from your laptop and trusting the result. A pass there says nothing about a cloud browser with another source IP and a different network view (steps 5 and 6).
  • putting credentials inside --proxy-server, or using an authenticated SOCKS5 proxy. As of September 2026, as far as I know, Chromium has no username and password auth for SOCKS5. Use the provider’s http endpoint instead.
  • rotating exits mid-session. Depending on the provider, a gateway that changes the exit IP on a timer or per request can drop an open tunnel. If a task needs one Singapore mobile IP for the whole session, for example checking your own Singapore-facing app as a local mobile user sees it, a sticky session is the fit. Singapore Mobile Proxy sells real Singapore mobile IPs with sticky sessions. It is Singapore-only, so it does nothing for you if you need another country.
  • retry loops with no ceiling. Three model-driven retries per URL adds up fast and you can’t see it without traces. Observing AI agents covers what to record.
  • reading a site’s rejection as a tunnel fault. A tunnel error page is Chrome’s own. If a page came back from the site, the proxy did its job, and whether your agent should be there is a question about the site’s terms and your users’ consent.

Scaling this

  • 10x: a handful of concurrent sessions. Providers cap concurrent connections per account, and it shows up as a 429 or 403 in step 4. Run the preflight per session and log the CONNECT status next to the session ID.
  • 100x: one bad endpoint can poison a whole batch. Keep a small pool, count tunnel failures per endpoint, and stop routing to one after a few in a row instead of retrying it. Plans meter bandwidth differently, so check yours. IP reputation starts to matter too, because a shared exit carries other people’s behaviour.
  • 1000x: proxy capacity becomes infrastructure. Alert on tunnel-failure rate per endpoint and per region, run the preflight from the same region as the browsers, and split workloads across accounts so one plan limit can’t stop everything. I’m not quoting limits because providers set them per plan and change them, so read yours as of today. Treat this as a design checklist, not a benchmark.

Where to go next

The full list lives on the blog index.

Written by Xavier Fok

disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-09-19.

free download
Why did my agent get blocked? A triage checklist

The checks we run, in order, when a browser or phone agent starts failing: network, fingerprint, behaviour, account. Leave your email and we will also tell you when we publish a new field note, a few times a month at most.

from the team behind this site
A Singapore mobile IP for browser agents

Singapore Mobile Proxy runs real mobile IPs on SingTel, StarHub and M1, with sticky sessions so one task keeps one IP. Singapore only: a fit for SEA or location-agnostic work, the wrong tool if you need a US IP.

see plans →
from the team behind this site
A real Android phone for phone-use agents

cloudf.one hosts real Android phones in Singapore on dedicated hardware, each with a persistent Singapore mobile IP. For agents that need an actual device and a stable carrier identity.

get a phone →
read on
More from The Agent Ops Report

Blocks, sessions, retries, traces, cost per task and phone-use agents. Browse all articles →