← Blog

BLOG

Playwright Remote Server: Connect to Hosted Chromium

Run Playwright against a remote server instead of local Chrome. Covers connectOverCDP, launch options, session isolation, and production trade-offs.

September 14, 20269 min readRemote Browser

# Playwright Remote Server: Connect to Hosted Chromium

A Playwright remote server lets your test or agent code drive a Chromium instance that runs somewhere other than your machine. Instead of bundling browser binaries into every container, you point Playwright at a WebSocket endpoint and call chromium.connectOverCDP(). The browser process, its profile, and its network stack live on the remote host; your code stays thin.

This guide covers what a Playwright remote server actually is, how the connection works at the protocol level, which launch options matter when the browser is not local, and the production criteria that separate a workable setup from one that breaks under load. If you want the runtime layer rather than the wiring, see Remote Browser for AI agents.

What "Playwright remote server" actually means

Playwright ships two ways to reach a browser:

  1. Local launch. chromium.launch() spawns a browser process on the same host as your script. Playwright manages the process lifecycle, downloads the binary via playwright install, and talks to it over a pipe or local WebSocket.
  2. Remote connect. chromium.connectOverCDP(endpointURL) attaches to a browser that is already running elsewhere. Playwright does not own the process. It speaks the Chrome DevTools Protocol over a WebSocket to whatever is listening on the other end.

The second mode is what people mean by a "Playwright remote server." The remote side exposes a CDP endpoint — typically ws://host:port/devtools/browser/<id> — and Playwright becomes a client of it.

This distinction matters because almost every operational difference follows from it. When you launch locally, you control the binary version, the flags, the profile directory, and the shutdown. When you connect remotely, you inherit whatever the server decided, and you need a contract with that server about lifecycle, cleanup, and isolation.

Playwright's own documentation on BrowserType.connectOverCDP is the authoritative reference for the client side. The Chrome DevTools Protocol spec is the authoritative reference for what the server must implement.

How the connection works

CDP is a JSON-over-WebSocket protocol. Each message is a command, an event, or a response. Playwright multiplexes its own abstractions — pages, contexts, frames, network interception — on top of that wire format.

A minimal remote connection looks like this:

import { chromium, Browser, BrowserContext, Page } from 'playwright';

async function connectToRemoteBrowser(cdpUrl: string): Promise<void> {
  // cdpUrl looks like: wss://<host>/cdp/<session-id>
  const browser: Browser = await chromium.connectOverCDP(cdpUrl, {
    timeout: 30_000,
    slowMo: 0,
  });

  // A remote browser may already have contexts open.
  // Reuse the first one instead of creating a new context,
  // otherwise you can end up with orphaned tabs.
  const contexts: BrowserContext[] = browser.contexts();
  const context: BrowserContext =
    contexts.length > 0 ? contexts[0] : await browser.newContext();

  const page: Page = context.pages()[0] ?? (await context.newPage());

  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  console.log('title:', await page.title());

  // Do NOT call browser.close() if the server owns the lifecycle.
  // Disconnect instead, so the remote session survives for reuse.
  await browser.close();
}

connectToRemoteBrowser(process.env.CDP_URL!).catch((err) => {
  console.error('remote connect failed:', err);
  process.exit(1);
});

Three details in that snippet are load-bearing:

  • `browser.contexts()` may be non-empty. A freshly launched local browser starts with zero contexts. A remote browser that has been reused, or that was started with a default context, may already have one. Creating a second context is legal but changes your isolation model.
  • `browser.close()` semantics differ. On a local launch, close() kills the process. On a CDP connection, it closes the connection. Whether the remote browser then shuts down depends on the server. If you want the session to persist for the next task, close the connection and let the server decide.
  • Timeouts are your problem. A local launch fails fast if the binary is missing. A remote connect fails slowly if the endpoint is unreachable, so set an explicit timeout.

Local launch vs. remote server: the trade-offs

DimensionLocal chromium.launch()Remote connectOverCDP()
Binary managementYou run playwright install per hostServer owns the Chromium build
Startup latencyProcess spawn + cold profileNetwork round trip to an existing session
IsolationOne process per launch, by defaultDepends on server session model
ScalingBound by host CPU/RAMBound by server capacity and your quota
DebuggingLocal DevTools, local tracesLive viewer or remote DevTools endpoint
Profile persistenceManual userDataDirServer-managed persistent profiles
Network egressYour host's IPServer's IP, or a configured proxy
Failure modeCrash kills your scriptDisconnect leaves an orphaned session

The table is not an argument for one over the other. It is a list of decisions you have to make explicitly. Local launch is simpler until you need more than one host's worth of browsers. Remote connect is simpler once you accept that the browser is infrastructure.

If you are weighing the two directly, Browser-As-A-Service vs Self-Hosted Playwright Infra goes deeper on the cost and operational model.

Launch options that still matter remotely

launchOptions are passed to chromium.launch(). When you connect over CDP, you are not launching, so most of them are ignored — the server already applied its own flags. But a few concepts carry over, and knowing which is which prevents a class of confusing bugs.

Applied at launch, ignored on connect:

  • headless — the server decided this when it started Chromium.
  • args — flags like --disable-gpu or --no-sandbox were set at process start.
  • executablePath — the server chose the binary.
  • channel — same.
  • userDataDir — the server owns the profile directory.

Still meaningful on connect:

  • timeout — how long to wait for the CDP handshake.
  • slowMo — client-side pacing of Playwright actions.
  • headers — extra HTTP headers for the connection itself, if your endpoint needs auth.
  • logger — client-side protocol logging, useful when the handshake fails.

Context-level options you can still set after connecting, via browser.newContext():

  • viewport, userAgent, locale, timezoneId
  • geolocation, permissions
  • extraHTTPHeaders
  • proxy — only if the server permits per-context proxying

The practical rule: anything that requires a new OS process is the server's job. Anything that is a property of a browsing context is yours.

If you are debugging why a flag you passed had no effect, that is usually the answer. See Playwright Browser Launch Options for the full flag surface and which ones only apply at process start.

Production criteria for a remote Playwright server

A CDP endpoint that works in a demo is not the same as one that works in production. These are the properties worth checking before you commit.

Session isolation

Two concurrent tasks must not share cookies, storage, or a page tree. Ask how the server scopes sessions: one browser process per session, one context per session, or something weaker. Context-level isolation is cheaper but leaks through shared process state — service workers, for instance, can cross context boundaries in some configurations.

Lifecycle contract

Who closes the browser, and when? If your script crashes mid-run, does the session get reaped or does it linger and consume quota? A server that exposes an explicit session TTL and a delete endpoint is easier to operate than one that relies on disconnect detection.

Persistent profiles

Some workloads need to stay logged in across runs. That requires a profile directory that survives session teardown. Check whether profiles are addressable by name, whether they are encrypted at rest, and whether two sessions can attach to the same profile simultaneously — the answer should be no.

Network egress

The browser's outbound IP is the server's IP unless you configure a proxy. For any workload that hits rate-limited or geo-restricted endpoints, proxy configuration is a first-class requirement, not a nice-to-have.

Observability

When a remote run fails, you need more than a stack trace. A live viewer, a session recording, or a downloadable trace makes the difference between a five-minute fix and an afternoon of guessing. Remote Browser exposes a live viewer for exactly this reason; Remote Control Browser covers the debugging workflow.

Usage controls

Browser time is metered. You want per-session visibility into how long a session ran and what it consumed, plus a hard cap so a runaway loop cannot burn through a budget. Current metering and limits are on the pricing page.

Connecting Playwright to a hosted runtime

Remote Browser exposes CDP endpoints for hosted Chromium sessions. The integration is the same connectOverCDP call shown above; what changes is that the endpoint is provisioned per session and the surrounding concerns — isolation, profiles, proxies, viewer, metering — are handled by the runtime rather than by your code.

A typical flow:

  1. Request a session through the API and receive a CDP URL.
  2. Call chromium.connectOverCDP(cdpUrl).
  3. Run your Playwright script or agent loop.
  4. Close the connection; the runtime reaps the session per its TTL.

Because the endpoint is standard CDP, the same code works with Puppeteer (puppeteer.connect({ browserWSEndpoint })) and Selenium's CDP bridge. You are not locked into a Playwright-specific SDK.

For the broader picture of what a hosted runtime provides beyond the connection string, see Remote Browser Online.

Common failure modes

`connectOverCDP` hangs. Usually a network path issue — the endpoint is reachable from your laptop but not from the container. Test with a raw WebSocket client before blaming Playwright.

`Protocol error: Target closed`. The remote browser exited. Check the session TTL and whether another client disconnected it.

Pages appear in the wrong context. You created a new context when the server had already opened one. Enumerate browser.contexts() first.

Version skew. Playwright's CDP client is tested against specific Chromium versions. A server running a much newer or older build can produce protocol mismatches that surface as missing methods. Pin your Playwright version and confirm the server's Chromium build.

Silent quota exhaustion. Sessions that are never explicitly closed still consume browser time until the TTL fires. Always close what you open.

When a remote server is the right call

Choose a remote Playwright server when:

  • You run more concurrent browsers than one host can hold.
  • You need browser sessions to outlive the process that created them.
  • You want persistent profiles without managing userDataDir on ephemeral infrastructure.
  • You need a consistent egress IP or proxy configuration.
  • You want a live viewer for debugging runs you cannot reproduce locally.

Stay local when:

  • You are writing and iterating on a script and want the fastest feedback loop.
  • Your workload is a single browser on a developer machine.
  • You need a browser build the remote server does not offer.

Most teams end up with both: local launch for development, remote connect for CI and production. The code difference is one function call, which is the point of the CDP abstraction.

Getting started

The shortest path is to provision a session, copy the CDP URL, and run the TypeScript snippet from earlier against it. From there, the work is in the operational details: session TTLs, profile naming, proxy selection, and knowing where your traces land when something fails.

Start with the documentation for the session API and connection details, and check pricing for current metering before you size a workload.