← Blog

BLOG

Install Playwright Browsers: Local vs Remote Runtime

Learn how to install Playwright browsers locally, when to download them manually, and when a remote browser runtime is the better fit for AI agents.

September 24, 20268 min readRemote Browser

# Install Playwright Browsers: Local vs Remote Runtime

To install Playwright browsers, run npx playwright install after installing the playwright package. That command downloads Chromium, Firefox, and WebKit binaries into a local cache directory and wires them up so chromium.launch() works. That is the whole answer for a laptop. It is rarely the whole answer for a production agent, a CI pipeline, or a fleet of browser-use workers, because the install step is where local Playwright setups start to leak time, disk, and reproducibility.

This guide covers the install mechanics, the manual download path, and the point at which you should stop installing browsers at all and connect to a hosted Chromium session instead. If you already know you want the remote path, skip to connecting over CDP.

What playwright install actually does

Playwright ships as two separate things: the Node (or Python) package that contains the API, and the browser binaries that the API drives. Installing the package does not install the browsers. You need both steps.

npm init -y
npm install -D playwright
npx playwright install

The second command resolves the browser revisions that match your installed Playwright version and downloads them. By default they land in a platform-specific cache:

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

Each browser revision gets its own directory, for example chromium-1148 or firefox-1465. Playwright pins exact revisions per release, so upgrading the npm package usually means downloading new binaries. That pinning is a feature — it is why a test that passes locally tends to pass in CI — but it also means every version bump is a fresh download.

Install only what you need

Downloading all three engines is wasteful if you only automate Chromium. You can scope the install:

npx playwright install chromium
npx playwright install chromium firefox
npx playwright install --with-deps chromium   # Linux: also installs OS libraries

--with-deps matters on bare Linux images. Playwright's browsers link against system libraries (fonts, audio, GTK, NSS) that minimal Docker images do not include. Without them you get launch failures that look like Playwright bugs but are missing shared objects.

Version pinning and reproducibility

Two environment variables control where binaries live and whether downloads are skipped:

  • PLAYWRIGHT_BROWSERS_PATH — override the cache location. Set it to 0 to install browsers inside node_modules instead of a shared cache, which is useful when you want the browser version to travel with the project.
  • PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 — install the package without fetching browsers. Useful when a later build stage handles the download.

For CI, the practical pattern is to cache the browser directory keyed on the Playwright version, then run npx playwright install --with-deps only on a cache miss. That turns a multi-hundred-megabyte download into a cache restore on most builds.

Downloading Playwright browsers manually

Sometimes the default installer is not an option: air-gapped networks, corporate proxies that block the CDN, or a base image you cannot modify. Playwright supports a manual path.

  1. Find the download host for your version. Playwright publishes binaries at https://playwright.azureedge.net/builds/ and mirrors them at https://playwright-akamai.azureedge.net/builds/. The exact revision numbers are listed in the browsers.json file inside the playwright-core package.
  2. Download the archive for your platform, for example chromium-linux.zip or chromium-mac-arm64.zip.
  3. Extract it into the cache directory under the matching revision folder name.
  4. Set PLAYWRIGHT_BROWSERS_PATH if you extracted somewhere non-standard.

You can also point Playwright at a custom download location with PLAYWRIGHT_DOWNLOAD_HOST and supply credentials via PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT for slow links. This is the supported way to route installs through an internal artifact mirror.

A manual download is a maintenance commitment. Every Playwright upgrade changes the revision list, so your mirror script has to track browsers.json or your builds will silently fall back to a stale browser. If you are doing this for more than one or two projects, it is worth asking whether you want to own browser distribution at all.

Where local installs break down

The install step is fine on a developer machine. It gets awkward in three specific situations.

Ephemeral compute. Serverless functions and short-lived containers start cold. A cold start that includes a browser download is measured in tens of seconds to minutes, not milliseconds. You either bake browsers into the image (large, slow to build, slow to pull) or accept the cold-start penalty.

Fleet consistency. Ten workers each running playwright install at boot is ten concurrent downloads of the same large artifact. Multiply by every deploy. The failure mode is not dramatic — it is a slow, flaky pipeline that nobody wants to debug.

Agent workloads. Browser-use style agents are long-running and stateful. They need persistent profiles, cookies that survive across steps, and often a live view so a human can watch or intervene. A locally launched browser inside a worker process is hard to inspect and dies with the process.

None of these are Playwright bugs. They are consequences of treating the browser as a local dependency when it is really a runtime.

Local install vs remote browser runtime

DimensionLocal playwright installHosted Chromium session
SetupDownload binaries per machine or imageConnect to a session URL
Cold startSeconds to minutes on fresh computeSession provisioning, no binary download
Version managementYou track Playwright + browser revisionsRuntime owns the browser build
ScalingOne browser per worker processSessions provisioned per task
StateDies with the process unless you persist itPersistent profiles available
DebuggingLocal trace files, screenshotsLive viewer plus CDP access
Network identityWhatever the host hasConfigurable proxy and browser settings
Best fitLocal dev, small CI, one-off scriptsAgents, fleets, long-running tasks

The honest read: for a test suite that runs on a fixed CI runner, local install is simpler and cheaper. For anything that spawns browsers per task, scales horizontally, or needs to survive a process restart, the install step is the wrong abstraction.

Connecting to a remote browser over CDP

Playwright can drive a browser it did not launch. chromium.connectOverCDP() takes a WebSocket endpoint and returns a Browser object. The remote runtime is responsible for having a browser running; you are responsible for the session.

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

async function runTask(cdpUrl: string): Promise<void> {
  // Connect to a hosted Chromium session instead of launching locally.
  const browser: Browser = await chromium.connectOverCDP(cdpUrl);

  // Reuse the existing context so cookies and storage persist.
  const context: BrowserContext = browser.contexts()[0] ?? (await browser.newContext());
  const page: Page = context.pages()[0] ?? (await context.newPage());

  try {
    await page.goto('https://example.com/login', { waitUntil: 'domcontentloaded' });
    await page.getByLabel('Email').fill(process.env.APP_EMAIL!);
    await page.getByLabel('Password').fill(process.env.APP_PASSWORD!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('**/dashboard');

    const title = await page.title();
    console.log(`Landed on: ${title}`);
  } finally {
    // Close the connection, not the remote browser, if the session is reused.
    await browser.close();
  }
}

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

Two details that trip people up:

  • connectOverCDP is Chromium-only. Firefox and WebKit do not expose a compatible CDP surface, so a remote CDP endpoint means a Chromium-family browser. If you need cross-engine coverage, that is a reason to keep a local install for those engines.
  • browser.close() on a CDP connection closes the connection. Whether it also terminates the remote browser depends on the runtime. Check your provider's session semantics before assuming cleanup.

The protocol underneath is the Chrome DevTools Protocol, and Playwright's CDP connection docs cover the API surface. If you want a deeper walkthrough of the connection model, see Agent-Browser CDP.

What to check before moving browsers off your machine

If you are evaluating a hosted runtime, these are the criteria that actually matter in production, in rough order of how often they cause pain.

Session lifecycle. Can you create, reuse, and explicitly terminate sessions? Does a session survive a client disconnect? Long agent tasks need the browser to outlive a dropped WebSocket.

Persistent profiles. Agents that log in once and act many times need storage state that persists across sessions. Ask how profiles are scoped and whether they are isolated between tasks.

Network controls. Proxy configuration, region selection, and configurable browser settings matter for sites that behave differently by geography or that rate-limit by IP. Be skeptical of any claim that is not documented — "stealth" is a marketing word until you can see the actual settings.

Observability. A live viewer is the difference between debugging an agent in five minutes and re-running it blind. CDP access gives you the same DevTools surface you would have locally.

Isolation. One session per task, with no shared cookies or storage, unless you deliberately opt into a shared profile.

Usage controls. You want to see browser time and session counts before the invoice arrives. Current metering details live on the pricing page.

Compatibility. Playwright, Puppeteer, and Selenium clients should all be able to connect. If a runtime only speaks its own SDK, you are locked into its abstractions.

A practical migration path

You do not have to rip out local Playwright to adopt a remote runtime. The cleanest sequence:

  1. Keep npx playwright install for local development and for any test that needs Firefox or WebKit.
  2. Move Chromium-based agent workloads to connectOverCDP against a hosted session.
  3. Store the CDP endpoint in configuration, not in code, so you can point at a local Chrome with --remote-debugging-port=9222 during development and a hosted session in production.
  4. Persist storage state through the runtime's profile mechanism rather than serializing cookies yourself.
  5. Add a live viewer to your debugging loop before you need it at 2 a.m.

The payoff is that "install Playwright browsers" stops being a step in your deploy pipeline. It becomes a local convenience, and production stops depending on a download succeeding at the worst possible moment.

For the broader architecture — why agents need a dedicated browser runtime rather than a browser binary — see Remote Browser for AI Agents. If you want to try the connection path end to end, the documentation covers session creation, CDP endpoints, and profile handling.