BLOG
Remote Chromium API JavaScript: Connect Over CDP
Connect a remote Chromium API from JavaScript over CDP WebSocket with Playwright or Puppeteer, plus session, timeout, and reconnect patterns.
# Remote Chromium API JavaScript: Connect Over CDP
A remote Chromium API in JavaScript is a hosted Chromium instance you control from your own Node.js or browser-side code over the Chrome DevTools Protocol (CDP). Instead of launching Chrome on your laptop or CI runner, you call an API to create a session, receive a CDP WebSocket endpoint, and attach Playwright or Puppeteer to it. This guide covers the connection mechanics, the JavaScript code, and the production criteria that separate a working demo from a reliable runtime. If you want the broader architecture first, read Remote Browser for AI agents.
The core idea is simple: Chromium exposes CDP over a WebSocket. A hosted runtime gives you that WebSocket URL plus lifecycle management — session creation, isolation, profiles, proxies, and teardown. Your JavaScript never installs a browser binary; it just connects.
What the remote Chromium API actually exposes
CDP is a JSON-over-WebSocket protocol. Every browser instance exposes two relevant endpoints:
- Browser-level WebSocket (
/devtools/browser/<id>): the root connection. Playwright'sconnectOverCDPand Puppeteer'sconnectboth use this. - Page-level WebSocket (
/devtools/page/<id>): a single tab. Useful for low-level tooling, less so for Playwright.
A hosted runtime wraps these behind an HTTP API. A typical flow:
POSTto create a session with options (viewport, proxy, profile, timeouts).- Receive a
cdpUrlorwebSocketDebuggerUrlin the response. - Connect your automation library to that URL.
- Run your script.
- Close the session (or let it expire) so you stop being billed.
The Chrome DevTools Protocol documentation is the authoritative reference for the domains you can call directly — Page, Runtime, Network, Target, Fetch, and so on. Most JavaScript users never touch raw CDP because Playwright and Puppeteer wrap it, but knowing the layer exists matters when you need something the wrapper doesn't expose, like intercepting a specific network event or evaluating in an isolated world.
Why "remote" changes the failure modes
Locally, Chrome either launches or it doesn't. Remotely, you add new failure classes: the session may not be ready when you connect, the WebSocket may drop mid-task, the proxy may change between sessions, or the profile may not have persisted. A production-grade remote Chromium API handles these at the runtime layer. Your JavaScript still needs defensive connection logic — covered below.
Connecting Playwright to remote Chromium over CDP
Playwright's chromium.connectOverCDP() is the standard entry point. It accepts either a browser WebSocket URL or an HTTP endpoint; if you pass HTTP, Playwright resolves the WebSocket for you. The Playwright CDP docs note that connectOverCDP is Chromium-only, which is fine here — the runtime is Chromium.
Here is a TypeScript example that creates a session through a generic HTTP API, connects over CDP, and runs a task with proper cleanup:
import { chromium, Browser, BrowserContext, Page } from "playwright";
interface SessionResponse {
id: string;
cdpUrl: string; // e.g. wss://.../devtools/browser/<id>
}
const API_BASE = process.env.REMOTE_BROWSER_API!;
const API_KEY = process.env.REMOTE_BROWSER_KEY!;
async function createSession(): Promise<SessionResponse> {
const res = await fetch(`${API_BASE}/sessions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
viewport: { width: 1280, height: 800 },
// Runtime-specific options: profile, proxy, timeouts.
// Check /documentation for the exact schema.
}),
});
if (!res.ok) throw new Error(`Session create failed: ${res.status}`);
return res.json();
}
async function connectWithRetry(
cdpUrl: string,
attempts = 5,
): Promise<Browser> {
let lastErr: unknown;
for (let i = 0; i < attempts; i++) {
try {
return await chromium.connectOverCDP(cdpUrl, { timeout: 15_000 });
} catch (err) {
lastErr = err;
await new Promise((r) => setTimeout(r, 500 * 2 ** i));
}
}
throw lastErr;
}
async function run() {
const session = await createSession();
let browser: Browser | undefined;
try {
browser = await connectWithRetry(session.cdpUrl);
// connectOverCDP attaches to existing contexts; reuse or create one.
const context: BrowserContext =
browser.contexts()[0] ?? (await browser.newContext());
const page: Page = context.pages()[0] ?? (await context.newPage());
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
const title = await page.title();
console.log("title:", title);
// Example of a raw CDP call through the Playwright session.
const cdp = await context.newCDPSession(page);
const { frameTree } = await cdp.send("Page.getFrameTree");
console.log("main frame:", frameTree.frame.id);
} finally {
// Disconnect without killing the remote browser if you want to reconnect.
await browser?.close();
// Then explicitly terminate the session so billing stops.
await fetch(`${API_BASE}/sessions/${session.id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${API_KEY}` },
});
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});Three details matter here. First, connectOverCDP does not launch a browser — it attaches. Second, browser.close() on a CDP connection disconnects your client; whether it terminates the remote browser depends on the runtime, so always call the session-delete endpoint explicitly. Third, the retry loop is not optional in production; session provisioning is asynchronous and the WebSocket may not accept connections on the first attempt.
Puppeteer equivalent
Puppeteer uses puppeteer.connect({ browserWSEndpoint }). The pattern is identical: create session, connect, run, disconnect, delete. If you are already on Puppeteer, the migration is a one-line change from puppeteer.launch() to puppeteer.connect(). See Puppeteer remote browser JavaScript for the parallel walkthrough.
Production criteria for a remote Chromium API
Not every hosted Chromium endpoint is a production runtime. Evaluate against these dimensions before committing.
| Criterion | What to check | Why it matters |
|---|---|---|
| CDP compatibility | Standard connectOverCDP / browserWSEndpoint works without patches | Avoids forked client libraries |
| Session isolation | Each session gets its own browser process or context | Prevents cross-tenant state leaks |
| Persistent profiles | Storage state survives across sessions | Keeps logins and cookies without re-auth |
| Proxy configuration | Per-session proxy settings, including residential | IP quality affects success rate |
| Live viewer | Real-time view of the running session | Debugging agents without screenshots |
| Usage controls | Per-session timeouts, concurrency limits, metering | Prevents runaway cost |
| Connection stability | Reconnect semantics, idle timeouts documented | Long agent tasks survive network blips |
| Pricing model | Per-browser-hour vs per-request, subscription vs metered | Cost predictability at scale |
Remote Browser provides hosted Chromium sessions with CDP access, Playwright/Puppeteer/Selenium compatibility, a live viewer, persistent profiles, configurable browser settings for proxy and stealth-related behavior, session isolation, and usage controls. For current limits and rates, see /pricing. Exact concurrency and metering details are documented there rather than assumed here.
The trade-off: managed vs self-hosted
Self-hosting Chromium means running browserless/chrome, Steel, or your own Playwright grid. You get full control and no per-hour fee, but you own the operational surface: process supervision, memory limits, zombie cleanup, proxy management, profile storage, and scaling. For a team running a handful of scripts, self-hosting is fine. For an agent workload that spins up dozens of parallel sessions with varying proxies and profiles, the operational cost usually exceeds the metered cost. The decision hinges on whether browser infrastructure is your product or a dependency of it.
JavaScript-specific pitfalls when driving remote Chromium
Do not assume the page exists. connectOverCDP attaches to whatever contexts the remote browser already has. A fresh session may have zero pages. Always check context.pages()[0] and create one if needed, as in the example above.
Handle WebSocket drops. CDP is stateful. If the socket closes mid-page.click(), Playwright throws. Wrap long agent loops in a reconnect-and-resume pattern, or keep individual steps short enough that a retry is cheap. Persist task state outside the browser so a reconnect can pick up where it left off.
Separate disconnect from termination. browser.close() and browser.disconnect() behave differently across libraries and runtimes. In Puppeteer, disconnect() leaves the browser running; close() may terminate it. In Playwright over CDP, browser.close() disconnects the client. Rely on the runtime's explicit session-delete API for teardown, not on client-side close semantics.
Watch for context reuse. If a persistent profile is enabled, browser.contexts()[0] may already contain pages from a prior session. Decide deliberately whether to reuse or create a fresh context.
Do not leak CDP sessions. Each context.newCDPSession(page) allocates a session. Detach it when done, or you accumulate listeners and memory on long-lived connections.
Timeouts are layered. There is a session-provisioning timeout, a WebSocket connect timeout, a navigation timeout, and a session idle timeout. Set all four explicitly. A default of "whatever the library picks" will bite you at 3 a.m.
When to use raw CDP instead of Playwright
Playwright covers most needs. Reach for raw CDP when you need:
- Network interception at the protocol level —
Fetch.enablewith request patterns, useful for blocking trackers or rewriting responses before the page sees them. - Performance metrics —
Performance.getMetricsfor CPU and memory sampling during a task. - Target management —
Target.createTargetandTarget.attachToTargetfor multi-tab orchestration outside Playwright's context model. - Isolated world evaluation —
Page.createIsolatedWorldto run scripts that cannot be touched by page JavaScript.
You can mix both: use Playwright for navigation and selectors, then open a CDPSession for the protocol-level operation. The example above shows Page.getFrameTree through a Playwright CDP session — the same pattern works for any domain.
Choosing a runtime for JavaScript agents
If your JavaScript code is an AI agent loop rather than a fixed script, the runtime requirements shift. Agents make unpredictable navigation decisions, so session stability, live viewing, and per-session cost control matter more than raw launch speed. A runtime that starts in 200ms but drops the WebSocket after 90 seconds is worse than one that starts in 2 seconds and stays up. For the agent-specific view, see Remote browser online and Remote control browser.
Practical selection checklist:
- Verify CDP compatibility first. Run a five-line
connectOverCDPscript against the endpoint before evaluating anything else. - Test reconnect behavior. Kill the network for 10 seconds mid-task and see whether the session survives.
- Confirm profile persistence. Log in, close the session, open a new one, and check whether the cookie is still there.
- Check the live viewer. If you cannot watch an agent run in real time, debugging becomes screenshot archaeology.
- Read the pricing page carefully. Per-hour metering, minimum session durations, and concurrency caps all affect real cost. See /pricing for Remote Browser's current terms.
Summary
A remote Chromium API in JavaScript is a CDP WebSocket plus lifecycle management. The code is short — connectOverCDP or puppeteer.connect against a session URL — but production reliability lives in the details: retry on connect, explicit session teardown, layered timeouts, and a runtime that isolates sessions, persists profiles, and meters usage honestly. Start with the connection, then evaluate the runtime against the criteria table above. For the full API surface, see the documentation.