BLOG
Puppeteer BrowserWSEndpoint Not Working: Fixes That Hold
Puppeteer browserWSEndpoint not working? Diagnose the real causes—stale WS URLs, missing ws:// scheme, CDP version drift—and fix them for production.
# Puppeteer BrowserWSEndpoint Not Working
If puppeteer.connect({ browserWSEndpoint }) is throwing Protocol error, hanging on connect, or silently attaching to a browser that dies mid-task, the problem is almost never Puppeteer itself. It is the endpoint string, the browser lifecycle behind it, or the CDP contract between them. This guide walks through the failure modes that actually show up in production, how to tell them apart, and what to change so the connection survives long-running agent workloads.
The short version: browserWSEndpoint is a WebSocket URL that points at a specific, still-running Chromium instance. If any of those three properties is wrong—scheme, host, or liveness—the connect call fails or degrades. Most "not working" reports are one of six root causes, and each has a distinct error signature.
What browserWSEndpoint Actually Is
browserWSEndpoint is the browser-level DevTools WebSocket URL. It looks like:
ws://127.0.0.1:9222/devtools/browser/6b1f3c2a-...Puppeteer uses it to attach to an already-running browser instead of launching one. That is the entire mechanism. There is no HTTP fallback, no retry logic, and no version negotiation beyond what CDP itself provides. When you pass a bad endpoint, Puppeteer does not guess what you meant—it fails.
This matters because the endpoint is a *lease*, not a permanent address. On a hosted runtime, the URL is valid for the lifetime of that browser session. Close the session, and the URL is dead. Reconnect to it later and you get a connection refused or a protocol error, not a helpful message.
For a broader look at how hosted sessions differ from local Chrome, see Remote Browser for AI Agents.
The Six Failure Modes
1. Wrong scheme: http:// or https:// instead of ws:// or wss://
This is the single most common mistake. Copying a dashboard URL or a REST endpoint into browserWSEndpoint produces:
Error: Protocol error (Target.getBrowserContexts): 'http://...' is not a valid WebSocket URLPuppeteer requires ws:// (plain) or wss:// (TLS). If your provider gives you an HTTPS endpoint, that is usually a *different* API—often a REST control plane—not the CDP WebSocket. Check the provider's docs for the specific field name; some expose wsEndpoint, some expose browserWSEndpoint, and some expose both with different semantics.
2. Stale or expired session URL
Hosted browsers are metered. Sessions end. A URL that worked yesterday returns ECONNREFUSED or WebSocket closed with code 1006 today. This is not a bug—it is the session lifecycle working as designed.
The fix is architectural: fetch a fresh endpoint per task, not once at process start. Do not cache the WS URL in a config file or environment variable for a long-running worker.
3. Missing --remote-debugging-port or bound to loopback
If you are connecting to your own Chrome, the browser must have been started with remote debugging enabled and bound to an address the client can reach. --remote-debugging-port=9222 alone binds to 127.0.0.1, which is unreachable from another container or host. You need --remote-debugging-address=0.0.0.0 as well, and a firewall rule that permits the connection.
This is also a security footgun. An open CDP port is full browser control. Never expose it to the public internet without authentication in front of it.
4. CDP version drift between Puppeteer and Chromium
Puppeteer ships with a bundled Chromium version it was tested against. When you connect to a *different* Chromium build, some CDP domains may not exist or may have changed shape. Symptoms include:
Protocol error (Page.setDownloadBehavior): 'Page.setDownloadBehavior' wasn't found- Methods that silently no-op
Target.createTargetfailing on newer or older browsers
The practical rule: pin Puppeteer to a version whose bundled Chromium is close to the remote browser's version, or use puppeteer-core and accept that you are responsible for compatibility. Check the Chrome DevTools Protocol documentation for the domain you depend on.
5. Connecting to a page target instead of the browser target
There are two WebSocket URLs in a running Chrome: the browser-level one (/devtools/browser/<id>) and per-page ones (/devtools/page/<id>). browserWSEndpoint expects the browser-level URL. Passing a page URL produces confusing errors because Puppeteer tries to speak browser-level CDP to a page-level socket.
If you only have a page URL, use puppeteer.connect({ browserWSEndpoint }) on the browser URL, or attach to the page via Target.attachToTarget after connecting to the browser.
6. Proxy, TLS, or network interception
wss:// endpoints behind a corporate proxy often fail because the proxy does not tunnel WebSocket upgrades. Symptoms are timeouts, not errors. Test with a raw WebSocket client before blaming Puppeteer:
# Quick liveness check — replace with your endpoint
npx wscat -c "wss://your-endpoint/devtools/browser/abc123"If wscat cannot connect, Puppeteer will not either.
Diagnostic Table
| Symptom | Likely cause | First check |
|---|---|---|
not a valid WebSocket URL | Wrong scheme (http/https) | Endpoint string starts with ws:// or wss:// |
ECONNREFUSED | Session expired or port closed | Fetch a fresh endpoint |
WebSocket closed code 1006 | Network drop, proxy, or session killed | Test with wscat |
Protocol error (X): not found | CDP version drift | Compare Puppeteer and Chromium versions |
Hangs on connect() | Proxy not tunneling WS upgrade | Bypass proxy or use wss:// |
| Connects, then dies mid-task | Session timeout or idle reaper | Check session TTL and keepalive |
Target closed immediately | Attached to page URL, not browser URL | Verify /devtools/browser/ in path |
A Working TypeScript Pattern
The pattern below fetches a fresh endpoint per task, connects with puppeteer-core, and disconnects cleanly. It avoids the two most common production mistakes: caching the WS URL and forgetting to disconnect.
import puppeteer, { Browser } from 'puppeteer-core';
interface SessionResponse {
browserWSEndpoint: string;
sessionId: string;
}
// Replace with your runtime's session-creation call.
async function createSession(): Promise<SessionResponse> {
const res = await fetch('https://your-runtime.example/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile: 'default' }),
});
if (!res.ok) throw new Error(`Session create failed: ${res.status}`);
return res.json() as Promise<SessionResponse>;
}
async function runTask(url: string): Promise<string> {
const { browserWSEndpoint, sessionId } = await createSession();
let browser: Browser | undefined;
try {
browser = await puppeteer.connect({
browserWSEndpoint,
defaultViewport: { width: 1280, height: 800 },
protocolTimeout: 180_000, // long agent tasks need headroom
});
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
const title = await page.title();
return title;
} finally {
// disconnect() detaches; it does not kill the remote browser.
if (browser) await browser.disconnect();
// Explicitly end the session so it stops metering.
await fetch(`https://your-runtime.example/sessions/${sessionId}`, {
method: 'DELETE',
});
}
}Two details worth noting. First, protocolTimeout defaults are often too short for agent workloads that pause on model calls; raise it deliberately. Second, disconnect() and session termination are different operations. Disconnecting without ending the session leaves a metered browser running.
For the Playwright equivalent of this pattern, see Remote Web Browser.
Why Hosted Endpoints Fail Differently Than Local Ones
Local Chrome endpoints fail loudly and immediately—the port is closed, you get ECONNREFUSED in milliseconds. Hosted endpoints fail in more interesting ways because there is infrastructure between you and the browser:
- Session reapers terminate idle browsers on a TTL. A long model call can exceed it.
- Load balancers may route the initial HTTP session-create call and the WebSocket upgrade to different backends if sticky sessions are not configured.
- TLS termination must support WebSocket upgrade headers. Some misconfigured proxies strip them.
- Metering may pause or throttle sessions that exceed a usage budget.
None of these are Puppeteer problems, and none are fixed by changing your Puppeteer code. They are runtime problems, and they are why the endpoint should be treated as a short-lived credential rather than a configuration value.
Production Criteria for a Remote Browser Endpoint
If you are evaluating a hosted runtime, these are the properties that determine whether browserWSEndpoint stays reliable under load:
- Fresh endpoint per session. The API should return a new WS URL for each session, scoped to that session's lifetime.
- Explicit session termination. You need a way to end a session and stop metering, separate from disconnecting.
- CDP compatibility surface. Confirm which CDP domains are exposed and whether the Chromium build matches your Puppeteer version.
- Configurable browser settings. Proxy configuration, viewport defaults, and locale/timezone should be settable per session rather than baked into an image.
- Live viewer. When a connection fails, being able to see the browser state is the difference between a five-minute fix and an hour of guessing.
- Persistent profiles. If your agent needs to stay logged in across sessions, profile persistence has to be a first-class feature, not a workaround.
Remote Browser provides hosted Chromium sessions with CDP access, Playwright/Puppeteer/Selenium compatibility, a live viewer, persistent profiles, and configurable browser settings. Current session limits and pricing are on the pricing page; the connection details are in the documentation.
Debugging Checklist
Work through this in order. It resolves the large majority of browserWSEndpoint failures.
- Print the endpoint. Confirm it starts with
ws://orwss://and contains/devtools/browser/. - Test with `wscat`. If the raw WebSocket cannot connect, Puppeteer cannot either.
- Create a fresh session. Do not reuse a URL from a previous run.
- Check Puppeteer and Chromium versions. Use
puppeteer-coreand pin deliberately. - Raise `protocolTimeout`. Agent tasks pause on model calls; the default is often too short.
- Verify proxy behavior. WebSocket upgrades need explicit proxy support.
- Confirm session termination. A leaked session keeps metering and may hit concurrency limits.
- Watch the live viewer. If the browser is alive but your code cannot attach, the problem is on the client side.
When to Stop Debugging and Change the Runtime
There is a point where the endpoint is fine and the architecture is wrong. If you are maintaining your own Chrome fleet—patching images, managing ports, restarting crashed browsers, and reconciling CDP versions across workers—you are paying an infrastructure tax that does not improve your agent. The endpoint is not the problem; owning the browser is.
The alternative is to treat the browser as a service you connect to, not a process you run. That shift is what Remote Control Browser covers in detail: the operational model where sessions are created per task, endpoints are short-lived, and the browser lifecycle is someone else's problem.
For teams running browser-use or similar agent frameworks, the practical migration is small. Replace the local puppeteer.launch() with a session-create call, pass the returned browserWSEndpoint to puppeteer.connect(), and make sure you terminate sessions in a finally block. Everything downstream—your page logic, your selectors, your extraction code—stays the same.
Summary
browserWSEndpoint not working is almost always one of six things: wrong scheme, expired session, loopback binding, CDP version drift, page-vs-browser URL confusion, or a proxy that will not tunnel WebSocket upgrades. Each has a distinct error signature, and the diagnostic table above maps symptoms to causes.
The durable fix is not a retry loop. It is treating the endpoint as a per-session credential: fetch it fresh, connect with puppeteer-core, raise protocolTimeout for agent workloads, and terminate the session explicitly when the task ends. Do that, and the connection failures that dominate local Chrome fleets stop being your problem.