BLOG
Puppeteer Remote Browser Javascript: Connect to Hosted Chromium
Learn how Puppeteer remote browser JavaScript works with hosted Chromium: connect via browserWSEndpoint, manage sessions, and run agents in production.
# Puppeteer Remote Browser Javascript: Connect to Hosted Chromium
Running Puppeteer against a remote browser in JavaScript means your script no longer launches Chrome on the machine executing the code. Instead, it opens a WebSocket connection to a Chromium instance running elsewhere and drives it over the Chrome DevTools Protocol (CDP). That single change — puppeteer.launch() becomes puppeteer.connect() — is what lets you move browser workloads off laptops, CI runners, and short-lived containers and onto a runtime that stays alive, keeps its profile, and can be watched in real time.
This guide covers the mechanics of Puppeteer remote browser JavaScript: how the connection actually works, what browserWSEndpoint is, how to structure sessions for AI agents, and the production criteria that separate a demo from something you can run continuously.
What "remote browser" means for Puppeteer
Puppeteer has two entry points:
puppeteer.launch()— spawns a local Chromium process and manages its lifecycle.puppeteer.connect()— attaches to an already-running Chromium over CDP.
A remote browser runtime is the second case. You get a WebSocket endpoint (or a CDP URL) for a hosted Chromium session, pass it to puppeteer.connect(), and Puppeteer treats that remote instance exactly like a local one. Pages, frames, network interception, evaluate(), screenshots — all of it works the same way, because it is the same protocol.
The practical difference is where the browser lives and who keeps it alive. With a hosted runtime, the browser process, its profile, and its network egress are managed for you. Your JavaScript is just a client.
If you want the broader framing of why this split matters for agent workloads, see Remote Browser for AI Agents.
How the CDP connection works
CDP is a JSON-over-WebSocket protocol. When Chromium starts with remote debugging enabled, it exposes an HTTP endpoint that lists debuggable targets and a WebSocket URL for each. Puppeteer's connect() takes that WebSocket URL and speaks CDP to it.
A typical flow:
- Your code requests a session from the runtime's API.
- The runtime returns a
browserWSEndpoint(and often a separate live-view URL). - Puppeteer connects, creates pages, and runs your automation.
- You close the browser handle — or leave the session running for a later reconnect.
The important detail: browser.disconnect() and browser.close() are different. disconnect() drops your client connection but leaves the remote browser alive. close() terminates it. For long-running agents that reconnect across steps, you almost always want disconnect().
The authoritative reference for the protocol itself is the Chrome DevTools Protocol documentation.
Connecting Puppeteer to a hosted Chromium session
Here is a minimal TypeScript example. It connects to a remote endpoint, navigates, extracts data, and disconnects without killing the session.
import puppeteer, { Browser, Page } from 'puppeteer-core';
interface SessionInfo {
browserWSEndpoint: string;
sessionId: string;
}
// Your runtime returns a CDP endpoint for a hosted Chromium session.
async function createSession(): Promise<SessionInfo> {
const res = await fetch('https://api.remote-browser.dev/sessions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.REMOTE_BROWSER_KEY}`,
},
body: JSON.stringify({ profile: 'agent-default' }),
});
if (!res.ok) throw new Error(`session create failed: ${res.status}`);
return (await res.json()) as SessionInfo;
}
async function run(): Promise<void> {
const { browserWSEndpoint, sessionId } = await createSession();
const browser: Browser = await puppeteer.connect({
browserWSEndpoint,
defaultViewport: { width: 1280, height: 800 },
});
try {
const page: Page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const title = await page.title();
const links = await page.$$eval('a', (anchors) =>
anchors.map((a) => (a as HTMLAnchorElement).href).slice(0, 10),
);
console.log({ sessionId, title, links });
} finally {
// Keep the remote session alive for later steps.
await browser.disconnect();
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});Two things to note. First, puppeteer-core is the right package here — you do not need the bundled Chromium download when you are connecting to a remote instance. Second, the finally block uses disconnect(), so the session survives for the next agent step.
Puppeteer vs Playwright for remote connections
Both libraries speak CDP, but they differ in ergonomics for remote work.
| Concern | Puppeteer | Playwright |
|---|---|---|
| Remote connect API | puppeteer.connect({ browserWSEndpoint }) | chromium.connectOverCDP(endpoint) |
| Protocol | CDP | CDP (Chromium), custom for Firefox/WebKit |
| Auto-waiting | Manual (waitForSelector, etc.) | Built into locators |
| Multi-browser | Chromium only | Chromium, Firefox, WebKit |
| Contexts | Browser contexts | Browser contexts + richer isolation |
| Best fit | Existing Puppeteer code, CDP-level control | New projects, cross-browser needs |
If you already have Puppeteer scripts, connect() is a small change. If you are starting fresh and want cross-browser coverage, Playwright's connectOverCDP is worth evaluating — the connection model is the same, only the client differs. For a deeper comparison of the connection paths, see Remote Web Browser.
Why remote browsers matter for AI agents
A browser agent is a loop: observe the page, decide an action, execute it, repeat. That loop has different infrastructure needs than a test suite.
- Session persistence. Agents often need to resume mid-task. A remote session that outlives your process means you can reconnect rather than restart from a login page.
- Live inspection. When an agent gets stuck, you need to see the DOM and the screen. A live viewer attached to the session is faster than replaying logs.
- Isolation. Each agent task should get its own browser context so cookies and storage do not leak between runs.
- Egress control. Protected sites care about where requests come from. Configurable proxy settings let you route traffic deliberately rather than hoping a datacenter IP passes.
These are runtime concerns, not model concerns. The model decides what to click; the runtime decides whether the click lands on a page that is still authenticated, still loaded, and still the right origin.
Production criteria for a Puppeteer remote runtime
Before you commit to a runtime, check these properties against your workload.
Connection stability. CDP is a long-lived WebSocket. Does the runtime handle reconnects, or does a dropped socket kill the session? Ask what happens on network blips.
Session lifecycle control. Can you create, reuse, and explicitly terminate sessions? Can you reconnect to a session by ID after your worker restarts? This matters for anything that runs longer than a single request.
Profile handling. Persistent profiles let an agent keep cookies and local storage across sessions. Ephemeral profiles keep runs clean. You want both, selectable per session.
Isolation guarantees. Confirm that separate sessions do not share storage, and that a crashed session cannot corrupt another.
Observability. A live viewer, session logs, and network capture are not luxuries when an agent fails silently at step 40.
Usage controls. Metering per browser-hour, concurrency limits, and spend caps. If a runtime cannot tell you what a session costs, you cannot budget agent runs. Current rates and limits are listed on the pricing page.
Protocol fidelity. The runtime should expose standard CDP, not a proprietary wrapper. If your Puppeteer code works locally, it should work remotely with only the connection line changed.
Common failure modes and how to avoid them
Leaking browser processes. If you call launch() in a loop and never close, you accumulate zombies. With remote sessions, the equivalent mistake is creating sessions and never terminating them. Track session IDs and clean up explicitly.
Assuming `close()` is safe mid-task. browser.close() ends the remote browser. If your agent has more steps, use disconnect() and reconnect later.
Ignoring navigation timing. waitUntil: 'networkidle2' is a heuristic. For SPAs that hydrate after load, wait on a specific selector or a network response instead.
Hardcoding viewport and user agent. Some sites behave differently at different viewports. Set these per session rather than relying on defaults, and keep them consistent across a task.
Treating CAPTCHAs as solved. No runtime guarantees CAPTCHA bypass. What a managed runtime can offer is configurable browser settings and proxy routing that reduce false positives. Plan for human-in-the-loop or a dedicated solving step when a challenge appears. For how this plays out in agent stacks, see Remote Browser Online.
Wiring Puppeteer into an agent loop
A practical pattern for agent use:
- Create a session with a named profile and proxy config.
- Connect with
puppeteer.connect(). - Run one step of the agent loop, then
disconnect(). - Persist the session ID alongside your task state.
- Reconnect on the next step using the same endpoint.
- Terminate the session when the task completes or fails permanently.
This keeps each step short-lived and stateless from your worker's perspective, while the browser state lives in the runtime. It also means a worker crash does not lose the browser — you just reconnect.
For teams running this at scale, the runtime becomes the shared substrate across Puppeteer, Playwright, and Selenium clients. The documentation covers the connection details for each.
When a local browser is still the right call
Remote runtimes are not universally better. Stay local when:
- You are debugging a selector and want DevTools attached directly.
- The workload is a one-off script that runs once.
- You need a browser extension that only works in a headed local Chrome.
- Network egress must come from a specific machine you control.
The decision is about lifecycle. If the browser needs to outlive your process, be shared across workers, or be inspected by someone other than the person running the script, remote is the better fit. If it is a throwaway, local is simpler.
Summary
Puppeteer remote browser JavaScript is not a different API — it is puppeteer.connect() pointed at a CDP endpoint instead of puppeteer.launch(). The work is in the runtime around it: session lifecycle, profile persistence, isolation, proxy configuration, and observability. Get those right and your Puppeteer code runs unchanged against hosted Chromium, whether it is a test suite or an agent loop that needs to survive a worker restart.
Start with the connection, then harden the lifecycle. The documentation has the endpoint and session details, and pricing covers metering if you are planning agent-scale usage.