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.
# 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 installThe 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 to0to install browsers insidenode_modulesinstead 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.
- Find the download host for your version. Playwright publishes binaries at
https://playwright.azureedge.net/builds/and mirrors them athttps://playwright-akamai.azureedge.net/builds/. The exact revision numbers are listed in thebrowsers.jsonfile inside theplaywright-corepackage. - Download the archive for your platform, for example
chromium-linux.ziporchromium-mac-arm64.zip. - Extract it into the cache directory under the matching revision folder name.
- Set
PLAYWRIGHT_BROWSERS_PATHif 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
| Dimension | Local playwright install | Hosted Chromium session |
|---|---|---|
| Setup | Download binaries per machine or image | Connect to a session URL |
| Cold start | Seconds to minutes on fresh compute | Session provisioning, no binary download |
| Version management | You track Playwright + browser revisions | Runtime owns the browser build |
| Scaling | One browser per worker process | Sessions provisioned per task |
| State | Dies with the process unless you persist it | Persistent profiles available |
| Debugging | Local trace files, screenshots | Live viewer plus CDP access |
| Network identity | Whatever the host has | Configurable proxy and browser settings |
| Best fit | Local dev, small CI, one-off scripts | Agents, 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:
connectOverCDPis 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:
- Keep
npx playwright installfor local development and for any test that needs Firefox or WebKit. - Move Chromium-based agent workloads to
connectOverCDPagainst a hosted session. - Store the CDP endpoint in configuration, not in code, so you can point at a local Chrome with
--remote-debugging-port=9222during development and a hosted session in production. - Persist storage state through the runtime's profile mechanism rather than serializing cookies yourself.
- 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.