← Blog

BLOG

Playwright Install Browsers Manually: A Practical Guide

Learn how to playwright install browsers manually, when to skip the download, and how to point Playwright at a remote Chromium over CDP instead.

September 13, 20269 min readRemote Browser

# Playwright Install Browsers Manually

If you need to playwright install browsers manually, the short answer is: run npx playwright install chromium (or firefox/webkit) to fetch the browser binaries Playwright expects, then verify the install path with npx playwright install --dry-run. The longer answer is that most teams doing this are really trying to solve one of three problems — pinning a specific browser build, running in a CI image where the default download fails, or avoiding the download entirely by connecting to a browser that already exists somewhere else. This guide covers all three, plus the CDP path that lets you skip local browser installs altogether.

Why "install browsers manually" is usually the wrong framing

Playwright ships with a CLI that manages browser binaries for you. npx playwright install downloads Chromium, Firefox, and WebKit into a cache directory (~/.cache/ms-playwright on Linux, ~/Library/Caches/ms-playwright on macOS, %USERPROFILE%\AppData\Local\ms-playwright on Windows). When people say they want to install "manually," they usually mean one of these:

  • The default install is failing. Corporate proxies, air-gapped CI, or a locked-down container block the CDN download.
  • They want a specific revision. Playwright pins browser builds per version, and sometimes you need to match a build to a known-good test baseline.
  • They don't actually want a local browser at all. They want to connect to a Chromium instance running elsewhere — a container, a VM, or a hosted runtime — and the local install is pure overhead.

The third case is the one worth thinking hardest about. If your automation runs in a serverless function, a short-lived CI job, or an agent loop that spins up and tears down sessions constantly, downloading a ~150MB Chromium binary on every cold start is a real cost. Connecting to an existing browser over the Chrome DevTools Protocol (CDP) removes that cost entirely.

The manual install commands, precisely

Playwright's install CLI is the supported path. Here's what each command does.

# Install all three browsers (Chromium, Firefox, WebKit)
npx playwright install

# Install only Chromium
npx playwright install chromium

# Install Chromium plus its OS-level dependencies (Linux only)
npx playwright install --with-deps chromium

# Install a specific browser version
npx playwright install chromium@1.47.0

# See what would be installed without downloading
npx playwright install --dry-run

# Force re-download even if the browser is cached
npx playwright install --force chromium

A few things that trip people up:

  • `--with-deps` only works on Linux. It shells out to apt-get to install shared libraries (fonts, codecs, libnss3, etc.). On macOS and Windows, the browser bundles its own dependencies.
  • Version pinning uses the Playwright version, not the Chromium version. chromium@1.47.0 means "the Chromium build that Playwright 1.47.0 expects," not "Chromium 1.47."
  • The cache is shared across projects. If two projects use different Playwright versions, they'll coexist in the same cache directory. This is usually fine, but it means --force affects everything.

If you're installing in a Docker image, the standard pattern is to install browsers during the image build, not at runtime:

FROM mcr.microsoft.com/playwright:v1.47.0-jammy
# Browsers are already installed in this base image.
# If you need a different version:
RUN npx playwright install --with-deps chromium

The official Playwright Docker images already contain the browsers, which is why most CI setups use them instead of running playwright install at all.

When the manual install fails

Three failure modes account for most broken installs:

Proxy or firewall blocks the CDN. Playwright downloads from playwright.azureedge.net and cdn.playwright.dev. If your network blocks those, set HTTPS_PROXY before running the install, or point PLAYWRIGHT_DOWNLOAD_HOST at an internal mirror.

Missing OS dependencies. On a bare Linux container, Chromium will install but fail to launch with errors about libnss3.so or libatk-1.0.so.0. --with-deps fixes this, but it requires root or sudo.

Architecture mismatch. Apple Silicon and ARM containers need the ARM builds. Playwright handles this automatically in recent versions, but older pinned versions may not.

If you're hitting any of these repeatedly, it's worth asking whether you need a local browser at all. That's where CDP comes in.

Skipping the install: connect to a remote browser over CDP

Playwright's chromium.connectOverCDP() method connects to any Chromium-based browser that exposes a CDP endpoint. The browser can be running on the same machine, in a container, on a VM, or in a hosted runtime. No local install required.

This is the pattern that makes sense for AI agents, CI jobs, and anything that runs in short-lived processes. Instead of installing a browser, you connect to one that's already running.

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

async function connectToRemoteBrowser(cdpUrl: string): Promise<Page> {
  // cdpUrl looks like: wss://your-runtime.example.com/cdp/session-id
  const browser: Browser = await chromium.connectOverCDP(cdpUrl, {
    timeout: 30_000,
  });

  // A remote browser may already have contexts open.
  // Reuse the first one instead of creating a new context.
  const contexts: BrowserContext[] = browser.contexts();
  const context: BrowserContext =
    contexts.length > 0 ? contexts[0] : await browser.newContext();

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

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

  // Do NOT call browser.close() on a shared remote browser —
  // it terminates the session for everyone using it.
  // Instead, disconnect:
  await browser.close();
  return page;
}

connectToRemoteBrowser(process.env.CDP_URL!).catch(console.error);

The key detail: connectOverCDP returns a Browser object, but calling browser.close() on a remote connection disconnects your client — it doesn't necessarily kill the remote browser. Behavior depends on the endpoint. If you're connecting to a shared session, treat close() as "disconnect me," and let the runtime manage the browser lifecycle.

connectOverCDP vs. launch: when to use which

Criterionchromium.launch()chromium.connectOverCDP()
Browser binaryMust be installed locallyNot required
Startup cost~1–3s per launchConnection only, ~100–500ms
Session persistenceLost on closeCan persist across connections
Profile/cookiesFresh by defaultReuses remote profile
Best forLocal dev, single-run testsCI, agents, long-lived sessions
DebuggingLocal DevToolsRemote viewer or CDP inspector
ScalingOne process per browserMany clients, one browser

For a single test run on your laptop, launch() is simpler. For anything that runs repeatedly, in parallel, or in an environment where you don't control the OS, connectOverCDP() is usually the better fit.

Common connectOverCDP failures and how to fix them

The related keyword "playwright connect to remote chrome not working" is common for a reason. Here are the failure modes and their causes.

`Protocol error (Browser.getVersion): Target closed` — The CDP endpoint isn't actually a browser. This usually means you pointed at an HTTP URL instead of a WebSocket URL, or the browser crashed on startup. Verify the endpoint responds to GET /json/version before connecting.

`connectOverCDP: Timeout` — The endpoint is reachable but not responding to CDP handshakes. Common causes: the browser was launched without --remote-debugging-port, or a proxy is intercepting the WebSocket upgrade.

`browser.contexts()` returns an empty array — The remote browser has no contexts open. This is normal for a freshly started browser. Create one with browser.newContext().

Pages disappear mid-session — The remote browser is being recycled. If you're on a hosted runtime, check whether your session has a timeout and whether you need to send keepalive traffic.

`connectOverCDP` works but `launch()` doesn't — This is the expected outcome when the local install is broken. It's also a signal that you should stop trying to fix the local install and commit to the remote path.

What "manual install" means in a hosted runtime

If you're using a hosted Chromium runtime like Remote Browser, the install question mostly disappears. You get a CDP URL, you connect, you work. The runtime handles browser binaries, OS dependencies, and session lifecycle.

What you still control:

  • Browser version. Hosted runtimes typically pin to a recent stable Chromium. If you need a specific build, check the runtime's docs.
  • Launch flags. If the runtime lets you pass launchOptions.args, you can configure things like --disable-blink-features=AutomationControlled or custom window sizes. See Playwright browser launch options for the full list.
  • Session persistence. Persistent profiles let cookies and localStorage survive across connections, which matters for agents that log in once and operate repeatedly.
  • Network configuration. Proxies and configurable browser settings are usually exposed as session parameters rather than launch flags.

The trade-off is real: you give up some control over the exact browser binary in exchange for not managing it. For most production workloads, that's the right trade.

Production criteria for choosing between local and remote

If you're deciding whether to keep installing browsers locally or move to a remote runtime, these are the questions that actually matter:

  1. How often does the browser start? If it's once per test run, local install is fine. If it's once per request or per agent step, the install cost dominates.
  2. Do you need a persistent profile? Local launch() gives you a fresh profile unless you configure userDataDir. Remote runtimes typically offer persistent profiles as a first-class feature.
  3. How many concurrent sessions? Local browsers are limited by the machine's RAM. A remote runtime scales independently of your worker.
  4. Do you need to debug live? A live viewer on a remote session is often easier than attaching DevTools to a headless local browser.
  5. What's your CI story? If your CI image already has browsers (like the official Playwright images), local is fine. If you're building custom images, remote removes a whole class of build failures.

For a deeper comparison, see Browser-As-A-Service vs Self-Hosted Playwright Infra.

A note on the Playwright Chrome extension

People searching for "playwright chrome extension" or "playwright mcp chrome extension" are usually looking for a way to drive their existing Chrome browser rather than a fresh Playwright-managed one. Playwright doesn't ship a Chrome extension. The supported way to control an existing Chrome instance is to launch it with --remote-debugging-port=9222 and connect via connectOverCDP('http://localhost:9222'). That's covered in detail in Playwright connect to existing browser.

The MCP angle is different: MCP servers that expose browser control typically wrap Playwright or CDP under the hood. They don't change the underlying connection model.

Summary

  • npx playwright install chromium is the supported manual install path. Use --with-deps on Linux and --force to bypass the cache.
  • Most "manual install" problems are proxy, dependency, or architecture issues — not Playwright bugs.
  • If you're installing browsers on every cold start, you're paying a cost you can avoid with connectOverCDP().
  • connectOverCDP() connects to any CDP endpoint: local Chrome, a container, or a hosted runtime. It's the right choice for CI, agents, and long-lived sessions.
  • Hosted runtimes remove the install step entirely. See Remote Browser online for how that works in practice, and check /pricing for current usage details.

For the full API surface, the Playwright CDP documentation is the authoritative reference. For everything else — sessions, profiles, and connecting code to hosted Chromium — start with the documentation.