← Blog

BLOG

Remote Chromium API Example: Connect Over CDP

A working remote Chromium API example: launch a hosted session, connect Playwright over CDP, and run a real task in TypeScript.

October 19, 20269 min readRemote Browser

# Remote Chromium API Example: Connect Over CDP

This is a working remote Chromium API example. You create a hosted Chromium session through an HTTP API, get back a CDP WebSocket endpoint, and connect Playwright or Puppeteer to it exactly as you would to a local Chrome started with --remote-debugging-port. The browser runs on someone else's infrastructure; your code runs anywhere with network access.

That pattern is the whole point of a remote Chromium API: you keep the automation library you already know, and you stop shipping a browser binary with your app. This guide walks through the request, the connection, a real TypeScript script, and the production details that decide whether it holds up under load.

What a remote Chromium API actually returns

A remote Chromium API is a thin control plane in front of real Chromium processes. The control plane does three things:

  1. Session lifecycle. You call an endpoint to create a browser session. You get a session ID and a CDP WebSocket URL. You call another endpoint to stop it.
  2. Transport. The WebSocket URL speaks the Chrome DevTools Protocol, the same protocol Chrome exposes locally. Playwright, Puppeteer, Selenium (via CDP bridges), and raw CDP clients all speak it.
  3. Runtime configuration. Proxies, persistent profiles, viewport, timeouts, and isolation are set at session creation, not in your client code.

The important consequence: there is no proprietary automation SDK to learn. If your script works against local Chrome over CDP, it works against a hosted session with one line changed.

What "remote" changes and what it doesn't

It does not change the protocol. It changes the failure modes. Locally, a crashed browser is your problem on your machine. Remotely, a crashed browser is a session that needs to be recreated, and your retry logic has to know that. Locally, you have one browser. Remotely, you have N concurrent sessions and need to track which script owns which.

It also changes what you can do about IP reputation and persistence. A hosted runtime can route sessions through configurable proxies and attach persistent profiles, which is hard to do consistently on a laptop or a short-lived CI runner. For a deeper look at why that matters for agents, see remote browsers for AI agents.

The remote Chromium API example, step by step

The flow is the same regardless of provider shape:

POST /sessions          -> { id, cdpUrl, ... }
connect(cdpUrl)         -> Playwright Browser
... run your automation ...
DELETE /sessions/{id}   -> session torn down

Exact paths and auth headers differ by provider. Remote Browser documents its endpoints in the documentation; the example below uses a generic shape so you can map it to whatever client you use.

Step 1: create a session

You send a small JSON body describing the browser you want. Typical fields:

  • viewport — width and height for the initial page.
  • proxy — a proxy URL or a provider-managed proxy setting.
  • profile — an identifier for a persistent profile, if you want cookies and storage to survive across sessions.
  • timeout — a hard cap so a stuck session doesn't bill forever.

Keep this request idempotent-friendly. If your create call times out, you don't know whether a session exists. Log the request ID and reconcile before retrying, or you'll leak sessions.

Step 2: connect over CDP

Playwright's chromium.connectOverCDP() takes the WebSocket URL and returns a Browser. From there, browser.contexts() gives you the existing context — hosted sessions usually start with one — and you create pages from it.

One detail that trips people up: when you connect over CDP, you are attaching to a browser that already exists. You don't call browser.newContext() unless you want an additional isolated context. The default context is already there, and it's the one carrying the profile and proxy settings you configured at session creation.

Step 3: run the task

This is ordinary Playwright. Navigate, wait for selectors, extract text, click. Nothing about the code is remote-specific.

Step 4: close cleanly

Call browser.close() and then the session-delete endpoint. browser.close() on a CDP connection disconnects your client; it may or may not terminate the remote browser depending on the provider. Always call the explicit stop endpoint so you're not paying for an idle session.

TypeScript example: Playwright over CDP

Here's a complete script. It creates a session, connects, scrapes a page, and tears down in a finally block so a thrown error doesn't leak the session.

import { chromium, type Browser } from "playwright";

const API_BASE = process.env.BROWSER_API_BASE!; // e.g. https://api.example.dev
const API_KEY = process.env.BROWSER_API_KEY!;

type Session = { id: string; cdpUrl: string };

async function createSession(): Promise<Session> {
  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 },
      timeoutSeconds: 300,
    }),
  });
  if (!res.ok) throw new Error(`create failed: ${res.status}`);
  return (await res.json()) as Session;
}

async function stopSession(id: string): Promise<void> {
  await fetch(`${API_BASE}/sessions/${id}`, {
    method: "DELETE",
    headers: { authorization: `Bearer ${API_KEY}` },
  });
}

async function main() {
  const session = await createSession();
  let browser: Browser | undefined;

  try {
    browser = await chromium.connectOverCDP(session.cdpUrl);

    // Reuse the context the hosted browser already created.
    const context = browser.contexts()[0] ?? (await browser.newContext());
    const page = await context.newPage();

    await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
    const title = await page.title();
    const heading = await page.locator("h1").first().innerText();

    console.log({ title, heading });
  } finally {
    await browser?.close().catch(() => {});
    await stopSession(session.id).catch(() => {});
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Two things to notice. First, connectOverCDP is the only remote-specific call. Everything after it is portable Playwright. Second, the finally block is not optional in production — it's the difference between a clean bill and a pile of orphaned sessions.

If you're using Puppeteer instead, the equivalent is puppeteer.connect({ browserWSEndpoint: cdpUrl }). Same protocol, same lifecycle.

Remote Chromium API options compared

Different providers expose the same CDP primitive with different wrappers. Here's how the common shapes differ, without inventing numbers.

ApproachWhat you getWhere it fitsTrade-off
Raw CDP over WebSocketFull protocol access, no SDKCustom clients, debugging, protocol-level workYou manage reconnects, targets, and lifecycle yourself
Playwright connectOverCDPFamiliar API, auto-waiting, locatorsMost automation and agent scriptsSome CDP-only features aren't surfaced by Playwright
Puppeteer connectMature CDP client, tight Chrome couplingExisting Puppeteer codebasesLess cross-browser abstraction
Provider SDK wrapperSession helpers, retries, typed configTeams that want less glue codeAnother dependency to track
Self-hosted ChromiumFull control, no per-hour costHigh-volume, stable workloadsYou own scaling, patching, proxies, and uptime

The right row depends on how much you want to own. For a single script, raw CDP or Playwright is fine. For a fleet of agents, the wrapper and the runtime behind it start to matter more than the client library. If you're weighing hosted versus self-managed, remote browser online covers the operational side.

Production criteria for a remote Chromium API

A working example is table stakes. These are the things that decide whether the API survives contact with real traffic.

Session isolation

Every session should be a separate browser process with its own profile directory. Shared state between sessions is the fastest way to get cross-contaminated cookies, leaked logins, and flaky tests that pass alone and fail together. Ask how isolation is enforced, not just whether it exists.

Persistent profiles

If your workflow logs in once and reuses the session, you need profiles that survive across sessions. That means storage state, cookies, and local storage keyed to a profile ID you control. Without it, every run starts cold and your login step becomes the bottleneck. The remote web browser guide goes deeper on profile semantics.

Proxy and network configuration

Proxy settings should be configurable per session, not global. Residential versus datacenter routing changes success rates on sites with bot detection, and you want to choose per target rather than per account. Be precise about what the provider actually offers — "configurable browser settings" is the honest framing unless a provider documents specific fingerprint controls.

Live viewer and debugging

When a remote session fails, you can't open DevTools on your laptop. A live viewer that streams the session, plus the ability to attach a CDP client mid-run, turns a black-box failure into a debuggable one. This is the single biggest quality-of-life difference between providers. See remote control browser for how live control works in practice.

Usage controls and cost visibility

Per-session timeouts, concurrency caps, and usage reporting are not nice-to-haves. A runaway loop that opens sessions without closing them is a real incident. Set hard timeouts at creation, monitor active session counts, and check pricing for how billing is metered before you scale.

Reconnect and retry semantics

CDP WebSockets drop. Networks flap. Your client needs a reconnect strategy that distinguishes "the browser is still alive, reconnect" from "the session is gone, recreate it." Most providers expose a session-status endpoint; poll it before deciding which path to take.

Common mistakes in remote Chromium API examples

  • Calling `browser.newContext()` on a CDP connection and ignoring the default context. You lose the profile and proxy you configured. Use browser.contexts()[0].
  • Not closing sessions in a `finally` block. Exceptions leak sessions. Always pair create with a guaranteed stop.
  • Hardcoding the CDP URL. Session URLs are per-session and often short-lived. Read them from the create response every time.
  • Assuming `browser.close()` stops billing. It disconnects the client. Call the explicit stop endpoint.
  • Ignoring `waitUntil`. page.goto defaults to load, which can hang on pages with long-polling resources. Use domcontentloaded or networkidle deliberately.
  • Skipping timeouts. A session with no timeout is a session that runs until someone notices.

When to use a remote Chromium API versus local Chrome

Use local Chrome when you're developing, when the target is on your intranet, or when you need to see the browser on your own screen. Use a remote Chromium API when:

  • Your automation runs in CI, serverless, or containers where installing Chromium is painful.
  • You need many concurrent sessions and don't want to manage a browser farm.
  • You need persistent profiles, proxy routing, or IP diversity that's awkward to reproduce locally.
  • You're running AI agents that need a browser they can drive over CDP from a model loop.

The trade-off is latency and dependency. A remote session adds a network hop to every CDP message, and you're trusting a third party with your session data. For latency-sensitive interactive work, measure round-trip time before committing. For batch automation and agent tasks, the hop is usually invisible next to page load times.

Wrapping up

The remote Chromium API example is short: create a session, connectOverCDP, run Playwright, stop the session. The hard part is everything around it — isolation, profiles, proxies, timeouts, and clean teardown. Get those right and the API is just Chrome with a longer cable.

Start with the documentation for the exact endpoints, check pricing for how sessions are metered, and keep the finally block.