BLOG
Playwright Connect To Remote Chrome Not Working
Playwright connect to remote Chrome not working? Diagnose CDP endpoint, WebSocket, and launch flag failures, then fix them with a hosted runtime.
# Playwright Connect To Remote Chrome Not Working
If Playwright connect to remote Chrome is not working, the failure is almost always one of five things: the remote Chrome was never started with a debugging port, the CDP endpoint is not reachable from your machine, the WebSocket URL is wrong, the browser is bound to localhost only, or you are calling connectOverCDP against a browser that has already exited. This guide walks through each failure mode, shows the exact error you will see, and gives you a working TypeScript fix. It also covers when to stop debugging your own Chrome instance and connect to a hosted Chromium session instead.
Why connectOverCDP Fails So Often
Playwright's chromium.connectOverCDP() does not launch a browser. It attaches to one that is already running and already exposing the Chrome DevTools Protocol over HTTP and WebSocket. That single design decision explains most of the pain: every prerequisite is external to Playwright, so Playwright's error messages describe the symptom, not the cause.
The three moving parts you must get right:
- The remote Chrome process must be running with
--remote-debugging-port(or--remote-debugging-pipe). - The CDP HTTP endpoint must be reachable over the network from wherever your Playwright script runs.
- The WebSocket handshake must succeed, which depends on host headers, proxies, and whether the browser is still alive.
Get any one wrong and you get a connection error that looks identical to the other two. Let's separate them.
Failure Mode 1: Chrome Was Not Started With a Debugging Port
This is the most common cause. A normal google-chrome launch does not open a CDP port. If you run:
google-chrome --headless=newand then try to connect, you will get something like:
Error: connect ECONNREFUSED 127.0.0.1:9222or, from Playwright:
browserType.connectOverCDP: Protocol error (Browser.getVersion): Target closedChrome only exposes CDP when you explicitly ask it to:
google-chrome \
--headless=new \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-cdp-profileTwo details matter here. First, --remote-debugging-port is required; there is no default. Second, since Chrome 136, --remote-debugging-port is ignored unless you also pass a non-default --user-data-dir. This is a security change that broke a lot of CI scripts. If your connection worked last year and stopped working after a Chrome upgrade, this is why.
Verify the port is actually open before blaming Playwright:
curl -s http://127.0.0.1:9222/json/versionYou should get a JSON blob with webSocketDebuggerUrl. If you get a connection refused, Chrome is not listening. If you get an empty response, Chrome is listening but the port is bound to a different interface.
Failure Mode 2: The Endpoint Is Not Reachable
connectOverCDP accepts either an HTTP endpoint or a full WebSocket URL. The HTTP form is more forgiving because Playwright fetches /json/version itself:
import { chromium } from 'playwright';
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');If your remote Chrome runs on another host, 127.0.0.1 is wrong from your perspective. You need either the remote host's IP or a tunnel. And here is the trap: Chrome binds the debugging port to `127.0.0.1` by default, even when you pass --remote-debugging-port=9222. From another machine, the port appears closed.
You have three options:
- SSH tunnel (safest for ad-hoc debugging):
ssh -L 9222:127.0.0.1:9222 user@remote-host, then connect tohttp://127.0.0.1:9222locally. - Bind to all interfaces: add
--remote-debugging-address=0.0.0.0. This exposes an unauthenticated remote-control port to your network. Do not do this on a shared or public network. - Use a managed endpoint that terminates the connection for you and handles auth.
The 0.0.0.0 route is where most "it works locally but not in CI" stories end. It also creates a real security problem: anyone who can reach that port can drive the browser, read cookies, and exfiltrate session data. If you need remote access at any scale, treat CDP exposure as an infrastructure concern, not a launch flag.
Failure Mode 3: Wrong WebSocket URL or Stale Target
If you skip the HTTP endpoint and pass a raw WebSocket URL, you own the correctness of that string:
const browser = await chromium.connectOverCDP(
'ws://127.0.0.1:9222/devtools/browser/6f3a...'
);Two things break here. First, the browser-level WebSocket ID changes every time Chrome restarts, so hardcoding it guarantees eventual failure. Second, if you accidentally grab a page-level WebSocket from /json/list instead of the browser-level one from /json/version, Playwright will connect and then behave strangely — browser.contexts() returns nothing useful, and new pages do not appear where you expect.
Rule of thumb: pass the HTTP endpoint and let Playwright resolve the WebSocket. Only pass a WebSocket URL when your provider hands you one directly.
Failure Mode 4: The Browser Already Exited
A headless Chrome started with --headless=new and no pages open will exit after a short idle period in some configurations. Your script connects, the process is gone, and you get:
Error: browserType.connectOverCDP: connect ECONNREFUSEDor a WebSocket close during the handshake. The fix is to keep the browser alive: open a page, or run Chrome under a supervisor that restarts it. This is one of the strongest arguments for a hosted runtime — the session lifecycle is managed for you, so "the browser died between my test setup and my test run" stops being a category of bug.
Failure Mode 5: Version and Protocol Mismatch
Playwright ships a pinned Chromium build. When you connect over CDP to a *different* Chrome version, most things work, but edge cases appear: newer CDP domains Playwright does not know about, or removed methods. You will see errors like Protocol error (SomeDomain.method): 'SomeDomain.method' wasn't found.
Practical mitigations:
- Keep your remote Chrome within a version or two of Playwright's bundled Chromium.
- Avoid calling raw CDP methods Playwright does not wrap unless you have tested them against your target version.
- Pin both sides in CI so upgrades are deliberate.
A Working TypeScript Example
Here is a complete, defensive connection that handles the common cases:
import { chromium, Browser, BrowserContext } from 'playwright';
async function connectToRemoteChrome(endpoint: string): Promise<{
browser: Browser;
context: BrowserContext;
}> {
// 1. Verify the CDP endpoint is alive before connecting.
const versionUrl = new URL('/json/version', endpoint).toString();
const res = await fetch(versionUrl);
if (!res.ok) {
throw new Error(`CDP endpoint unreachable: ${res.status} ${versionUrl}`);
}
const { Browser: version } = await res.json();
console.log(`Connecting to ${version}`);
// 2. Connect over CDP. Pass the HTTP endpoint, not a raw WebSocket.
const browser = await chromium.connectOverCDP(endpoint, {
timeout: 30_000,
});
// 3. Reuse the existing context if one exists; otherwise create one.
const contexts = browser.contexts();
const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
// 4. Fail loudly if the browser dies mid-run.
browser.on('disconnected', () => {
console.error('Remote browser disconnected — check session lifetime.');
});
return { browser, context };
}
const { browser, context } = await connectToRemoteChrome(
process.env.CDP_ENDPOINT ?? 'http://127.0.0.1:9222'
);
const page = await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close(); // For connectOverCDP, this disconnects; it does not kill the remote browser.Note the last line: with connectOverCDP, browser.close() disconnects your client. Whether the remote browser keeps running depends on who owns the process. That distinction trips people up constantly in CI.
Local Chrome vs Hosted Chromium: What Actually Changes
| Concern | Self-managed remote Chrome | Hosted Chromium session |
|---|---|---|
| Launch flags | You own --remote-debugging-port, --user-data-dir, --headless | Managed; you pass connection settings |
| Network reachability | You own tunnels, firewall, 0.0.0.0 exposure | Provider exposes an authenticated endpoint |
| Session lifetime | You supervise the process | Session lifecycle managed per connection |
| Profiles / cookies | Manual --user-data-dir management | Persistent profiles available |
| Proxy configuration | Manual per-launch flags | Configurable browser settings |
| Live debugging | VNC or port-forward to the host | Live viewer in the browser |
| Scaling | One process per worker, you schedule | Sessions provisioned on demand |
| Version drift | You pin Chrome yourself | Provider pins the Chromium build |
The trade-off is control versus operational surface. If you need a specific Chrome build with custom flags and you have the infra team to run it, self-managed is legitimate. If your actual goal is "my Playwright script connects and does work," the launch-flag debugging loop is pure overhead.
When to Stop Debugging and Use a Hosted Endpoint
You should switch to a hosted runtime when any of these are true:
- You are spending more time on CDP connectivity than on the automation itself.
- You need sessions to survive across workers, restarts, or long-running agent loops.
- You need persistent profiles so logins and cookies carry across runs.
- You need proxy or network configuration without rebuilding your launch command.
- You want to watch a session live while it runs, not reconstruct it from logs.
Remote Browser provides hosted Chromium sessions with CDP access and Playwright, Puppeteer, and Selenium compatibility. You get a connection endpoint, a live viewer, persistent profiles, session isolation, and configurable browser settings. The connection code is the same connectOverCDP call you already wrote — the difference is that the endpoint is reachable, authenticated, and managed.
const browser = await chromium.connectOverCDP(process.env.REMOTE_BROWSER_CDP_URL!);That is the entire migration for most scripts. For a deeper look at the runtime model, see Remote Browser for AI agents and the documentation. If you are evaluating cost before committing, pricing breaks down browser-hour metering.
Debugging Checklist
Run through this in order before you change any code:
- Is Chrome running with `--remote-debugging-port`? Check the process command line.
- Did you pass a non-default `--user-data-dir`? Required on Chrome 136+.
- Does `curl http://host:9222/json/version` return JSON? If not, it is a network or launch problem, not Playwright.
- Are you connecting from the same host? If not, is the port bound to
0.0.0.0or tunneled? - Are you passing the browser-level WebSocket, not a page-level one? Prefer the HTTP endpoint.
- Is the browser still alive when you connect? Add a keepalive or use a managed session.
- Do the Chrome and Playwright versions match closely? Pin both.
- Does `browser.close()` kill the remote browser? With
connectOverCDP, it does not — verify who owns the process.
Common Misconceptions
"Playwright can launch a remote browser." It cannot. connectOverCDP attaches. Launching is your responsibility or your provider's.
"I need the Playwright Chrome extension." You do not. The Playwright browser extension is a recorder tool for generating selectors, not a connection mechanism. It has nothing to do with connectOverCDP. If you are looking for a Chrome extension to bridge Playwright to a remote browser, you are solving the wrong problem — CDP is the bridge.
"`connectOverCDP` works with Firefox and WebKit." It does not. connectOverCDP is Chromium-only. Firefox and WebKit use Playwright's own protocol, not CDP. If you need cross-browser remote sessions, that is a different architecture.
"I can expose port 9222 publicly and add auth later." CDP has no built-in authentication. Exposing it is equivalent to handing over the browser. Use a tunnel or a managed endpoint.
Where to Go Next
If you have worked through the checklist and your connection still fails, the problem is almost certainly environmental: a firewall, a stale process, or a version mismatch. Those are solvable, but they are not the interesting part of your project.
The interesting part is what your automation does once it is connected. If you would rather spend your time there, start with a hosted session and skip the launch-flag archaeology. Read the documentation to get an endpoint, or compare approaches in Remote Browser online and Remote web browser.
For the authoritative reference on the protocol itself, see the Chrome DevTools Protocol documentation and Playwright's CDP guide.