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.
# 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 chromiumA few things that trip people up:
- `--with-deps` only works on Linux. It shells out to
apt-getto 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.0means "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
--forceaffects 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 chromiumThe 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
| Criterion | chromium.launch() | chromium.connectOverCDP() |
|---|---|---|
| Browser binary | Must be installed locally | Not required |
| Startup cost | ~1–3s per launch | Connection only, ~100–500ms |
| Session persistence | Lost on close | Can persist across connections |
| Profile/cookies | Fresh by default | Reuses remote profile |
| Best for | Local dev, single-run tests | CI, agents, long-lived sessions |
| Debugging | Local DevTools | Remote viewer or CDP inspector |
| Scaling | One process per browser | Many 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=AutomationControlledor 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:
- 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.
- Do you need a persistent profile? Local
launch()gives you a fresh profile unless you configureuserDataDir. Remote runtimes typically offer persistent profiles as a first-class feature. - How many concurrent sessions? Local browsers are limited by the machine's RAM. A remote runtime scales independently of your worker.
- Do you need to debug live? A live viewer on a remote session is often easier than attaching DevTools to a headless local browser.
- 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 chromiumis the supported manual install path. Use--with-depson Linux and--forceto 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.