← Blog

BLOG

Playwright Browsers Download: Local vs Remote Runtime

Playwright browsers download is only half the setup. Learn what gets installed, why it breaks in CI, and when a remote browser runtime is the better path.

September 24, 20269 min readRemote Browser

# Playwright Browsers Download: Local vs Remote Runtime

The playwright browsers download step is where most Playwright setups quietly become infrastructure projects. You run npx playwright install, wait for a few hundred megabytes of Chromium, Firefox, and WebKit to land in a cache directory, and everything works — until you move to CI, a container, or a machine that can't reach the CDN. This guide covers what the download actually does, where it fails, and how to decide between managing browser binaries yourself and connecting to a hosted runtime instead.

If your goal is to run browser automation in production rather than on your laptop, the download step is usually the wrong thing to optimize. Remote Browser provides hosted Chromium sessions with CDP access, so you can skip binary management entirely and connect over a WebSocket endpoint. You can read more about that model in Remote browsers for AI agents.

What playwright install actually downloads

Playwright does not ship browsers inside the npm package. The playwright and @playwright/test packages contain the driver and the API surface; the browser binaries are downloaded separately and cached on disk.

When you run:

npx playwright install

Playwright resolves the browser revisions pinned to your installed version and downloads each one. By default that means Chromium, Firefox, and WebKit, plus the headless shell variants in recent versions. Each browser is a full build, not a thin wrapper, so the total footprint is measured in several hundred megabytes.

The cache location depends on your OS:

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

You can override this with the PLAYWRIGHT_BROWSERS_PATH environment variable, which matters in Docker images and CI runners where you want the cache baked into a layer rather than downloaded on every job.

Two details trip people up:

  1. Browser revisions are pinned to the Playwright version. Upgrading Playwright can require a fresh download because the expected Chromium build changed. A package-lock.json bump is not just a JS dependency change.
  2. System dependencies are separate. On Linux, npx playwright install-deps installs the shared libraries Chromium and Firefox need. A browser binary with missing libnss3 or libatk will fail at launch with an error that looks unrelated to the download.

Why the download breaks in production

The download step is fine on a developer laptop with a warm cache and a fast connection. It becomes fragile in the environments where automation actually runs.

Ephemeral CI runners. Every job starts cold. If you don't cache ~/.cache/ms-playwright, you pay the download cost on every run. On a busy pipeline that's minutes of wall-clock time per job, multiplied across a matrix.

Restricted networks. Corporate proxies, air-gapped build environments, and egress-filtered clusters often block the Playwright CDN. The install fails, and the fix is either a mirror, a prebuilt image, or a different approach entirely.

Container image size. Baking three browsers plus system dependencies into an image adds significant size and rebuild time. Teams end up maintaining custom base images just to keep the download out of the critical path.

Version drift. A developer on Playwright 1.4x and a CI runner on 1.3x have different pinned browser revisions. Tests that pass locally fail in CI for reasons that have nothing to do with the test.

Scaling concurrency. Even with a warm cache, each browser instance consumes CPU and memory on the host. Running dozens of parallel sessions on one machine means the download was never the real constraint — resource contention is.

Manual download and offline installs

If you need to control the download, Playwright supports a few escape hatches.

# Install only Chromium
npx playwright install chromium

# Install with system dependencies on Linux
npx playwright install --with-deps chromium

# Point the cache at a custom location
export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
npx playwright install chromium

For offline environments, you can download the browser archives on a connected machine and place them in the cache directory in the expected layout, or use PLAYWRIGHT_DOWNLOAD_HOST to point at an internal mirror. Both approaches work, but they add a maintenance surface: you now own mirror availability, archive integrity, and the mapping between Playwright versions and browser revisions.

This is the point where many teams ask whether they should be managing browser binaries at all. If the browser is a runtime dependency for an agent or a test harness, treating it as a service rather than an artifact is often the cleaner split.

Local download vs remote runtime

The decision is not "local is bad, remote is good." It's about which constraints you want to own.

DimensionLocal playwright installRemote browser runtime
SetupDownload binaries per machine or imageConnect to a CDP endpoint
CI costCache management or per-job downloadNo binary download in the job
Version controlPinned to Playwright version, must stay in syncRuntime owns the browser build
ScalingBounded by host CPU/memorySessions run on managed infrastructure
Network requirementsAccess to Playwright CDN or mirrorAccess to the runtime endpoint
DebuggingLocal traces, screenshots, headed modeLive viewer plus CDP-level access
Best fitLocal dev, small test suites, full controlAgents, CI at scale, multi-tenant workloads

If your workload is a handful of tests on a developer machine, local install is the right answer and always will be. If you're running agents that need persistent profiles, isolated sessions, and predictable concurrency, the download step is overhead you don't need to carry.

Connecting Playwright to a hosted browser

The key insight is that Playwright doesn't require a locally installed browser to drive one. chromium.connectOverCDP() attaches to any Chromium instance exposing a DevTools Protocol endpoint. That endpoint can be on localhost or on the other side of the internet.

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

// The endpoint comes from your runtime provider.
// Treat it as a secret; it grants control of the session.
const CDP_ENDPOINT = process.env.REMOTE_BROWSER_CDP_URL!;

async function runTask(): Promise<void> {
  let browser: Browser | undefined;

  try {
    browser = await chromium.connectOverCDP(CDP_ENDPOINT, {
      timeout: 30_000,
    });

    // A hosted session usually starts with one context already open.
    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',
      timeout: 45_000,
    });

    const title = await page.title();
    console.log('loaded:', title);

    // Do not call browser.close() on a shared hosted session
    // unless you intend to terminate it. Disconnect instead.
    await browser.close();
  } catch (err) {
    console.error('session failed:', err);
    throw err;
  }
}

runTask();

A few production notes that the docs don't always make obvious:

  • `connectOverCDP` is Chromium-only. Firefox and WebKit are not reachable this way. If your test matrix needs all three engines, you either keep local installs for those or accept Chromium-only for the remote path. The Playwright CDP documentation is the authoritative reference here.
  • Don't close what you don't own. Calling browser.close() on a hosted session can terminate it for other consumers if the session is shared. Disconnect and let the runtime manage lifecycle.
  • Contexts may already exist. A freshly provisioned session often has a default context and page. Check browser.contexts() before creating new ones, or you'll leak contexts.
  • Timeouts are your problem. Network latency to a remote endpoint is real. Set explicit timeouts on connect and navigation rather than relying on defaults tuned for localhost.

If you're wiring this into an agent rather than a test suite, the same connection pattern applies — the difference is that the agent decides what to do on the page rather than a script. See Remote browser online for the runtime-side view of that workflow.

What a hosted runtime gives you beyond the download

Skipping the binary download is the entry-level benefit. The more durable reasons to use a hosted runtime show up later in the project lifecycle.

Session isolation. Each session runs in its own browser process with its own profile directory. Two agents running concurrently don't share cookies, local storage, or cache unless you explicitly want them to.

Persistent profiles. Some workflows need to stay logged in across sessions. A hosted runtime can persist a profile and reattach it, which is difficult to do reliably with ephemeral local installs.

Live viewer. When an agent fails at step 14 of a 20-step task, a screenshot is not enough. A live view of the session lets you watch what the browser actually did, which shortens debugging cycles considerably.

Configurable browser settings. Proxies, user agent, locale, timezone, and viewport are all configurable per session. If you need to match a specific environment, you set it at session creation rather than patching launch arguments.

Usage controls. Sessions are metered, which makes cost predictable in a way that "one more VM" is not. Current rates and limits are on the pricing page.

CDP access. You're not locked into a proprietary API. Anything that speaks CDP — Playwright, Puppeteer, Selenium, or a raw WebSocket client — can drive the session. That matters when you want to swap tooling without re-architecting.

When to keep local installs

Remote runtimes are not universally better. Keep playwright install local when:

  • You're developing and iterating on selectors, where headed mode and fast feedback matter.
  • Your test suite is small and runs on a single machine.
  • You need Firefox or WebKit coverage and can't split the matrix.
  • You have strict data residency requirements that a hosted runtime can't satisfy.
  • You want zero external dependencies in your build.

The pragmatic pattern many teams land on is a split: local installs for development and engine-specific tests, remote sessions for CI, agents, and anything that needs to scale horizontally. The Playwright API is the same in both cases, so the switch is a connection change, not a rewrite.

Practical checklist

Before you decide, answer these:

  1. How often does the download run? Once per developer machine is fine. Once per CI job is a cost.
  2. Do you need all three engines? If Chromium-only is acceptable, the remote path is simpler.
  3. What's your concurrency ceiling? If it's bounded by host resources, the download was never the bottleneck.
  4. Do sessions need to persist? Persistent profiles are easier on a managed runtime.
  5. How do you debug failures? If you're reconstructing agent behavior from logs, a live viewer pays for itself.

For a deeper look at how hosted Chromium fits into agent workflows, see Remote web browser and the documentation for connection details and session configuration.

The download command isn't the problem. Treating browser binaries as something every environment must fetch, patch, and version independently is. Decide which side of that line your workload sits on, and the rest of the setup follows.