← Blog

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.

October 19, 20268 min readRemote Browser

# 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's connectOverCDP and Puppeteer's connect both 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:

  1. POST to create a session with options (viewport, proxy, profile, timeouts).
  2. Receive a cdpUrl or webSocketDebuggerUrl in the response.
  3. Connect your automation library to that URL.
  4. Run your script.
  5. 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.

CriterionWhat to checkWhy it matters
CDP compatibilityStandard connectOverCDP / browserWSEndpoint works without patchesAvoids forked client libraries
Session isolationEach session gets its own browser process or contextPrevents cross-tenant state leaks
Persistent profilesStorage state survives across sessionsKeeps logins and cookies without re-auth
Proxy configurationPer-session proxy settings, including residentialIP quality affects success rate
Live viewerReal-time view of the running sessionDebugging agents without screenshots
Usage controlsPer-session timeouts, concurrency limits, meteringPrevents runaway cost
Connection stabilityReconnect semantics, idle timeouts documentedLong agent tasks survive network blips
Pricing modelPer-browser-hour vs per-request, subscription vs meteredCost 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.enable with request patterns, useful for blocking trackers or rewriting responses before the page sees them.
  • Performance metrics — Performance.getMetrics for CPU and memory sampling during a task.
  • Target management — Target.createTarget and Target.attachToTarget for multi-tab orchestration outside Playwright's context model.
  • Isolated world evaluation — Page.createIsolatedWorld to 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 connectOverCDP script 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.