← Blog

BLOG

Playwright Install Browsers: Local vs Remote Runtime

Learn how playwright install browsers works, when you need local binaries, and how to connect Playwright to a hosted Chromium runtime instead.

September 13, 20269 min readRemote Browser

# Playwright Install Browsers: Local vs Remote Runtime

playwright install browsers is the command that downloads the browser binaries Playwright drives — Chromium, Firefox, and WebKit — into a local cache. It is the first thing most teams run after npm install playwright, and it is also the step that causes the most friction once automation moves off a laptop and into CI, containers, or an agent runtime. This guide explains what the command actually does, when you can skip it, and how to connect Playwright to a hosted Chromium session when you would rather not ship browser binaries at all.

What playwright install browsers actually does

Playwright does not use the Chrome or Firefox already on your machine. It ships its own patched builds, pinned to a specific version, and stores them in a platform-specific cache directory:

  • macOS: ~/Library/Caches/ms-playwright
  • Linux: ~/.cache/ms-playwright
  • Windows: %USERPROFILE%\AppData\Local\ms-playwright

Running npx playwright install downloads every browser for your platform. Running npx playwright install chromium downloads only Chromium. The playwright npm package itself is small; the binaries are large — often several hundred megabytes each — which is why the install step is separate.

Two details matter in production:

  1. Version pinning is per-Playwright-release. Upgrading the playwright package usually requires re-running the install command, because the expected browser revision changes. A container that caches binaries across a dependency bump will fail at launch with a "browser not found" error.
  2. System dependencies are separate. On Linux, npx playwright install --with-deps also installs the shared libraries (fonts, codecs, libnss3, and so on) that headless Chromium needs. Without them, the binary downloads fine and then crashes on launch.

If you have ever seen Executable doesn't exist at .../chrome-linux/chrome, that is this mechanism failing — either the install never ran in that environment, or it ran against a different Playwright version.

When you do not need to install browsers locally

The install step exists because Playwright launches a browser process. If the browser process is already running somewhere else, you do not need a local binary — you need a connection string.

This is the model behind browserType.connectOverCDP() and browserType.connect(). Instead of spawning Chromium, Playwright attaches to an existing one over the Chrome DevTools Protocol. The local install becomes optional, and in some setups it becomes unnecessary entirely.

That distinction matters for three common situations:

  • CI pipelines where a 400 MB download per job is slow and cache invalidation is painful.
  • Containers where you would rather not maintain a base image with the full set of Chromium system libraries.
  • AI agent runtimes where the browser needs to outlive the process that started it, hold a persistent profile, or run behind a specific network egress.

For the last case, a hosted runtime like Remote Browser exposes a CDP endpoint you connect to directly. The Playwright client library still runs in your process; the browser does not.

Local install vs remote connection

DimensionLocal playwright installRemote CDP connection
Browser binaryDownloaded per machine or imageRuns in the hosted runtime
Setup cost~400 MB per browser, plus system depsOne connection URL
Version couplingTied to the playwright package versionRuntime manages the browser build
Session lifetimeDies with the Node processSurvives worker restarts
Profile persistenceManual userDataDir managementManaged persistent profiles
Network identityYour machine's IPConfigurable proxy settings
DebuggingLocal trace viewer, headed modeLive viewer over the session
ScalingOne process per browserSession isolation per workload

The trade-off is straightforward. Local install gives you full control and zero network dependency, at the cost of image size, cache management, and version drift. Remote connection trades that operational surface for a network hop and a dependency on the runtime's availability.

Neither is universally correct. A single-developer test suite that runs on one machine is fine with a local install. A fleet of agents that each need an isolated browser with a stable profile is not.

Connecting Playwright to a remote Chromium over CDP

The connection path is short. You need a CDP WebSocket URL from your runtime, and you pass it to chromium.connectOverCDP(). Note that CDP connection is Chromium-only — Firefox and WebKit do not expose a compatible endpoint, so connectOverCDP will not work with them.

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

// The runtime returns a CDP endpoint for a freshly created session.
// Treat this like a credential: it grants control of a live browser.
const CDP_ENDPOINT = process.env.REMOTE_BROWSER_CDP_URL!;

async function runTask(): Promise<void> {
  const browser: Browser = await chromium.connectOverCDP(CDP_ENDPOINT, {
    // Slow down actions slightly; useful when the target site is
    // sensitive to machine-speed interaction.
    slowMo: 50,
  });

  // A remote session usually starts with one context already open.
  // Reuse it rather than creating a new one, so cookies and storage
  // from the session's persistent profile are available.
  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);

  // Close the connection, not the browser. The hosted session can
  // outlive this process if you want to resume it later.
  await browser.close();
}

runTask().catch((err) => {
  console.error('task failed:', err);
  process.exitCode = 1;
});

Three things to get right:

  • Do not call `browser.close()` expecting to kill the remote browser. Over CDP, close() disconnects the client. Whether the session terminates depends on the runtime's session policy. If you want deterministic teardown, use the runtime's session API.
  • Reuse the existing context. Calling newContext() on a CDP connection creates an isolated context inside the same browser process. That is sometimes what you want, but it will not share cookies with the default context.
  • Handle reconnects. A long-running agent will eventually hit a dropped WebSocket. Wrap the connection in retry logic and re-fetch the endpoint rather than caching it indefinitely.

For the lower-level protocol details, the Chrome DevTools Protocol documentation is the authoritative reference for the domains Playwright exposes through context.newCDPSession(page).

Common failure modes after install

Most "Playwright is broken" reports trace back to one of these:

`Executable doesn't exist` — the install ran in a different environment, or the Playwright version changed after the binaries were cached. Fix: re-run npx playwright install in the same image or job that launches the browser.

Missing shared libraries on Linux — the binary exists but exits immediately. Fix: npx playwright install --with-deps, or install the documented system packages in your Dockerfile.

`connectOverCDP` hangs or returns 404 — the endpoint is wrong, expired, or points at a non-Chromium browser. CDP endpoints are typically short-lived and session-scoped; a stale URL will not reconnect.

Works locally, fails in CI — usually a headless/sandbox mismatch. Chromium in containers often needs --no-sandbox or a properly configured user namespace. These are launch arguments, not install problems, and they are worth understanding separately — see our notes on Playwright browser launch options for the arguments that matter in production.

Version drift between client and browser — Playwright's protocol expectations are tied to specific browser revisions. Connecting to a browser build far ahead of your client version can produce subtle failures rather than clean errors.

Where a hosted runtime changes the calculus

If you are running browser automation as a service — agents, scheduled jobs, or a test harness that many people depend on — the install step is a symptom of a larger problem: you are managing browser infrastructure as a side effect of managing application code.

A hosted runtime moves that responsibility. Concretely, what you get instead of a local install:

  • A CDP endpoint per session, so Playwright connects without downloading anything.
  • Persistent profiles, so authentication state survives across sessions and worker restarts.
  • Session isolation, so one workload's cookies and storage do not leak into another's.
  • Configurable browser and network settings, including proxy configuration, for workloads that need a specific egress identity.
  • A live viewer, so you can watch a session in progress instead of reconstructing it from traces.
  • Usage controls, so browser time is metered and attributable rather than an opaque line item on a VM bill.

The Playwright client code barely changes. You swap chromium.launch() for chromium.connectOverCDP(), and you stop maintaining a browser cache in your image.

This is the same shift described in Remote Browser for AI agents: the browser becomes a runtime you connect to, not a binary you ship.

A practical decision checklist

Choose a local install when:

  • You run tests on developer machines and a small number of CI jobs.
  • You need offline execution.
  • You want to test against a specific browser build you control.
  • Your workload is short-lived and does not need persistent state.

Choose a remote CDP connection when:

  • Browser sessions must outlive the process that created them.
  • You need persistent profiles or a specific network identity.
  • You are running many concurrent, isolated sessions.
  • You want to avoid rebuilding container images every time Playwright bumps a browser revision.
  • You need to observe or debug live sessions rather than post-hoc traces.

Many teams run both: local installs for fast unit-level checks, remote sessions for anything that touches real sites, real logins, or real concurrency.

Getting the connection details right

Two operational details are easy to get wrong and expensive to debug.

Endpoint lifetime. A CDP URL is a capability. Anyone holding it can drive the browser. Fetch it at the start of a task, do not log it, and do not persist it beyond the session it belongs to.

Session teardown. Decide explicitly whether a session should end when your client disconnects. For agent workloads, the useful default is usually "keep the session alive" so a crashed worker can reconnect and resume. For test runs, the useful default is the opposite. Make this a deliberate choice in your code rather than an accident of which API you called.

If you are evaluating this pattern for the first time, the documentation covers the connection flow end to end, and pricing explains how browser time is metered. Both are worth reading before you commit to an architecture — the cost model and the session model tend to drive the design more than the API surface does.

Summary

playwright install browsers downloads pinned browser binaries into a local cache. It is necessary whenever Playwright launches the browser itself, and it is the source of most environment-specific failures: missing system libraries, stale caches after version bumps, and container images that grow every release.

When the browser runs somewhere else, the install step disappears. chromium.connectOverCDP() attaches Playwright to a live Chromium over the DevTools Protocol, which is Chromium-only but sufficient for the vast majority of automation and agent workloads. The trade-off is a network dependency and a session model you have to reason about explicitly — in exchange for persistent profiles, session isolation, configurable network settings, and no browser binaries in your build.