BLOG
Puppeteer BrowserWSEndpoint GitHub: Connect to Remote Chromium
Puppeteer browserWSEndpoint GitHub examples explained: how to connect to hosted Chromium over CDP, what the endpoint URL contains, and production gotchas.
# Puppeteer BrowserWSEndpoint GitHub: Connect to Remote Chromium
If you searched for puppeteer browserwsendpoint github, you are probably looking at a puppeteer.connect({ browserWSEndpoint }) call in someone's repository and trying to work out two things: what that WebSocket URL actually is, and whether you can point it at a browser that does not live on your laptop. The short answer is that browserWSEndpoint is the DevTools WebSocket address of an already-running Chromium instance, and yes, you can point it at a hosted browser as long as that browser exposes CDP over a reachable WebSocket.
This guide covers what the endpoint contains, how GitHub examples typically wire it up, how to run the same pattern against a hosted runtime, and the failure modes that show up once you move from a local script to production.
What browserWSEndpoint Actually Is
Puppeteer has two ways to get a browser. puppeteer.launch() starts a new Chromium process and manages its lifecycle. puppeteer.connect() attaches to a browser that is already running. The browserWSEndpoint option is the address used by the second path.
When Chromium starts with remote debugging enabled, it opens a WebSocket server that speaks the Chrome DevTools Protocol. The endpoint looks like this:
ws://127.0.0.1:9222/devtools/browser/6f3c1a2e-9b4d-4f1a-8c2e-7d5a0b3e9f11Three parts matter:
- Scheme and host —
ws://orwss://, plus the host and port where the browser is listening. - Path —
/devtools/browser/followed by a browser-level UUID. This is not the same as a page target ID. - Transport — a persistent WebSocket, not HTTP. You cannot
curlit and expect a useful response.
The UUID is generated per browser process. Restart the browser and the endpoint changes. That single fact is the root cause of most "it worked yesterday" bugs in GitHub issues about browserWSEndpoint.
Why GitHub Examples Use It
Search GitHub for browserWSEndpoint and you will find the same three patterns repeatedly:
- Attaching to a manually started Chrome. The developer runs
chrome --remote-debugging-port=9222, then connects Puppeteer to it. Useful for debugging because you can watch the window. - Connecting to a Docker container. A
browserless/chromeor similar container publishes port 9222, and the test suite connects over the network. - Connecting to a cloud browser provider. The endpoint comes from an API response rather than a local process.
The third pattern is where most production teams end up, because it removes the need to run and patch Chromium yourself.
Getting the Endpoint: Local vs Hosted
The mechanics of puppeteer.connect() do not change between local and hosted. What changes is where the URL comes from and what you have to manage around it.
| Concern | Local Chromium | Hosted Chromium |
|---|---|---|
| Endpoint source | http://127.0.0.1:9222/json/version | Returned by the runtime's session API |
| Lifecycle | You start and stop the process | Managed by the runtime |
| Reachability | Localhost only | Public wss:// URL |
| Scaling | One process per machine | Session per task, isolated |
| Profile persistence | Manual --user-data-dir | Configurable per session |
| Network egress | Your IP | Configurable proxy settings |
| Debugging | Open the window | Live viewer or CDP |
| Cost model | Your compute | Metered browser time |
For local Chromium, you fetch the endpoint from the HTTP debug port:
curl -s http://127.0.0.1:9222/json/version | jq -r '.webSocketDebuggerUrl'That returns the ws:// URL you pass to Puppeteer. Hosted runtimes skip this step — the session creation response includes the WebSocket URL directly, usually as wss:// so the connection is encrypted in transit.
If you want the broader picture of how hosted sessions differ from a local Chrome install, the remote browser overview covers session lifecycle and isolation in more depth.
A Working Puppeteer Connect Example
Here is the minimal pattern you will see in most GitHub repositories, adapted to a hosted endpoint:
const puppeteer = require('puppeteer-core');
async function run() {
// In production this comes from your runtime's session API.
const browserWSEndpoint = process.env.BROWSER_WS_ENDPOINT;
const browser = await puppeteer.connect({
browserWSEndpoint,
defaultViewport: null,
});
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const title = await page.title();
console.log('title:', title);
// Close the page, but do not kill a browser you do not own.
await page.close();
await browser.disconnect();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});Two details are easy to get wrong:
- Use `puppeteer-core`, not `puppeteer`. The full
puppeteerpackage downloads a bundled Chromium on install. If you are connecting to a remote browser, that download is wasted disk and CI time.puppeteer-corehas no bundled browser. - Call `disconnect()`, not `close()`.
browser.close()sends a shutdown command to the browser process. On a hosted runtime that terminates the session, which may be what you want at the end of a task but is almost never what you want mid-run.
Playwright Equivalent
If your stack is Playwright rather than Puppeteer, the same CDP endpoint works through connectOverCDP. This is the pattern most AI agent frameworks use, because it lets you keep Playwright's locator API while the browser runs elsewhere:
import { chromium, Browser, Page } from 'playwright';
async function runTask(endpoint: string): Promise<void> {
// endpoint is a wss:// CDP URL from the hosted runtime
const browser: Browser = await chromium.connectOverCDP(endpoint, {
timeout: 30_000,
});
const context = browser.contexts()[0] ?? (await browser.newContext());
const page: Page = context.pages()[0] ?? (await context.newPage());
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.getByRole('link', { name: /docs/i }).first().click();
await page.waitForLoadState('networkidle');
console.log('url:', page.url());
// Detach without destroying the remote browser.
await browser.close();
}
runTask(process.env.BROWSER_WS_ENDPOINT!).catch((err) => {
console.error('task failed:', err);
process.exit(1);
});Note that browser.close() in Playwright over CDP detaches the client; whether the remote session is torn down depends on the runtime's session policy. Check your provider's docs — with Remote Browser, session teardown is explicit and separate from client disconnect.
The Playwright CDP documentation is the authoritative reference for connectOverCDP options, including headers and timeout.
Why "browserWSEndpoint GitHub" Searches Usually Mean Something Broke
Most people land on this query because a copied snippet stopped working. The failure modes are consistent enough to list.
The endpoint expired
Browser-level UUIDs are per-process. If the remote browser restarted, recycled, or the session timed out, the old URL returns a 404 or a connection refused. Symptom: Protocol error (Target.setDiscoverTargets): Target closed or an immediate WebSocket handshake failure.
Fix: fetch a fresh endpoint per task. Never cache a browserWSEndpoint in a config file or environment variable that outlives a single session.
You used ws:// against a remote host
Local Chrome exposes ws://127.0.0.1:9222. Hosted runtimes expose wss://. If you strip the s or the provider requires TLS and you send plain WebSocket, the handshake fails. Some proxies also reject ws:// upgrade requests outright.
The endpoint is a page target, not a browser target
/json/list returns page targets with their own webSocketDebuggerUrl values. Those are page-level endpoints. Passing one to puppeteer.connect({ browserWSEndpoint }) produces confusing errors because Puppeteer expects a browser-level endpoint. Use /json/version for the browser endpoint, or take the URL the runtime gives you.
Concurrency limits
A single browser process can host many pages, but there is a practical ceiling on how many. If you fan out 200 concurrent tasks against one endpoint, you will hit memory pressure and see timeouts that look like network problems. The fix is session-per-task rather than page-per-task, which is what hosted runtimes are built for. See remote browser for AI agents for how session isolation changes the concurrency math.
Version mismatch
Puppeteer's CDP client is versioned against a Chromium release. Connecting a very old puppeteer-core to a very new Chromium can break on protocol methods that changed shape. Pin your client version and check the runtime's Chromium version before upgrading either side.
Production Criteria for a Remote CDP Endpoint
If you are evaluating a hosted runtime to replace a local Chrome process, these are the questions that actually determine whether it survives contact with production.
Session isolation. Each task should get its own browser context or process. Shared state between tasks causes cookie bleed, cache collisions, and non-reproducible failures.
Persistent profiles. Some workflows need to stay logged in across sessions. Others need a clean slate every time. The runtime should let you choose per session rather than forcing one model.
Proxy and network settings. Egress IP affects what sites will serve you. Configurable proxy settings per session are the difference between a workflow that works and one that gets challenged on every request.
Live debugging. When a task fails at step 7 of 12, you need to see the DOM at that moment. A live viewer or an attachable CDP endpoint lets you inspect without re-running.
Usage controls. Browser time is the unit that scales with your workload. Understand how sessions are metered before you commit to an architecture. Current details are on the pricing page.
Client compatibility. Puppeteer, Playwright, and Selenium all speak CDP. A runtime that only supports one of them locks you in unnecessarily.
Connecting from an Agent Framework
The browserWSEndpoint pattern shows up in AI agent stacks for the same reason it shows up in test suites: the agent needs a browser, and it should not be the agent's job to install and patch one.
A typical agent loop looks like this:
- Request a session from the runtime API.
- Receive a
wss://CDP endpoint plus a session ID. - Connect with Playwright or Puppeteer.
- Run the task — navigate, extract, click, fill.
- Disconnect the client.
- Release the session so browser time stops accruing.
Step 6 is the one people forget. If your agent crashes before releasing, you keep paying for a browser nobody is using. Wrap session creation in a try/finally and release in the finally block.
If you are driving the browser from a remote control plane rather than a local script — for example, an agent running in a different region from the browser — the remote control browser guide covers latency and routing considerations.
Common Questions, Answered Directly
Can I use `browserWSEndpoint` with a hosted browser? Yes, provided the runtime exposes CDP over a reachable WebSocket. That is the standard integration path.
Do I need to install Chromium locally? No. Use puppeteer-core and connect to the remote endpoint. This is the main reason teams move to hosted browsers in CI.
What is the difference between `browserWSEndpoint` and `browserURL`? browserURL points at the HTTP debug port (http://host:9222) and Puppeteer fetches the WebSocket URL itself. browserWSEndpoint skips that step. Both work; the WebSocket form is one fewer round trip.
Why does my endpoint work locally but not from CI? Almost always a network policy issue — outbound WebSocket upgrades blocked, or the endpoint is bound to localhost on the CI runner rather than a reachable host.
Is the endpoint stable across reconnects? No. Treat it as ephemeral. Fetch it per session.
Where to Go Next
The browserWSEndpoint pattern is small — one option on one function — but it is the seam where local automation becomes infrastructure. Once you are connecting to a remote endpoint, you inherit questions about session lifecycle, isolation, profiles, and metering that a local Chrome process never forced you to answer.
Start with the documentation for the connection flow and session API, and check pricing to understand how browser time is metered before you size a workload. If you are coming from a local Puppeteer setup and want the migration path, the remote web browser guide walks through the differences in practice.