BLOG
Playwright Remote Browser Download: What You Actually Install
Playwright remote browser download explained: what gets installed locally, what runs in the cloud, and how to connect over CDP to hosted Chromium.
# Playwright Remote Browser Download: What You Actually Install
If you searched for a "playwright remote browser download," you probably want one of two things: a browser binary you can point Playwright at from somewhere else, or a way to stop downloading browser binaries on every machine that runs your tests. Those are different problems, and conflating them is where most setup guides go wrong.
Here is the short answer. Playwright does not ship a "remote browser" you download. It ships a CLI that downloads local browser builds, and a connectOverCDP API that attaches to a browser running anywhere else. A remote browser download is really a connection URL plus a CDP endpoint — the browser itself lives on infrastructure you don't manage. This guide covers both paths, when each makes sense, and the production details that bite people after the demo works.
What "Playwright remote browser download" usually means
Playwright's install model is local-first. npm i -D @playwright/test installs the test runner and the playwright package. The browsers come separately:
npx playwright install # all supported browsers
npx playwright install chromium # just Chromium
npx playwright install --with-deps chromium # plus OS librariesThat command pulls browser builds into a cache directory (~/.cache/ms-playwright on Linux, ~/Library/Caches/ms-playwright on macOS, %USERPROFILE%\AppData\Local\ms-playwright on Windows). Each Playwright version pins specific browser revisions, so upgrading Playwright usually triggers a fresh download.
When people say they want a "remote browser download," they're typically reacting to one of these:
- CI images are bloated. Every runner downloads ~150–400 MB of browser binaries per job unless you cache them.
- The target environment isn't where the code runs. You want to drive a browser from a laptop, a container, or an agent process that has no display server.
- You need a browser you can't install. Managed Chromium with proxy routing, session isolation, or a live viewer isn't something
playwright installgives you. - You're running AI agents. An agent loop that spawns a browser per task doesn't want to manage binary versions across a fleet.
The fix in all four cases is the same shape: keep Playwright as the client library, and connect it to a browser that already exists somewhere else. No local browser download required.
Local install vs. remote connect: the trade-off
| Dimension | Local playwright install | Remote browser over CDP |
|---|---|---|
| Browser binary | Downloaded per machine/version | Runs on hosted infrastructure |
| Startup cost | Cache hit is fast; cold install is slow | Connection handshake, no install |
| Version pinning | Tied to your Playwright version | Managed by the provider |
| Scaling | One browser per process, bounded by host RAM | Sessions provisioned per request |
| Debugging | Local trace viewer, headed mode | Live viewer, session replay |
| Network identity | Your machine's IP | Configurable proxy and browser settings |
| Best for | Unit tests, local dev, tight iteration | CI fleets, agents, geo-distributed runs |
Neither column is universally better. Local installs give you the tightest feedback loop and zero network dependency. Remote connections give you reproducibility and scale without shipping browser binaries into every environment.
The mistake is treating remote as a drop-in replacement for local in dev. It isn't. Keep local installs for fast iteration, and use remote sessions where the constraints actually apply.
How Playwright connects to a remote browser
Playwright's remote path is browserType.connectOverCDP(). You give it a WebSocket endpoint that speaks the Chrome DevTools Protocol, and it returns a Browser object you drive exactly like a locally launched one.
import { chromium, Browser, BrowserContext, Page } from 'playwright';
const CDP_URL = process.env.REMOTE_BROWSER_CDP_URL!;
async function runTask(): Promise<void> {
const browser: Browser = await chromium.connectOverCDP(CDP_URL, {
timeout: 30_000,
});
// A hosted session usually arrives with a context already created.
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', { waitUntil: 'domcontentloaded' });
await page.getByRole('link', { name: 'More information' }).click();
console.log('title:', await page.title());
} finally {
// Close the connection, not the remote browser, unless you own the session.
await browser.close();
}
}
runTask().catch((err) => {
console.error(err);
process.exit(1);
});Three details matter here:
- `connectOverCDP` is Chromium-only. Firefox and WebKit do not expose a CDP endpoint. If your matrix includes them, remote connection isn't the path — see the Playwright CDP docs for the current support statement.
- `browser.close()` closes the connection. Whether it also tears down the remote browser depends on the provider. Read the session lifecycle docs before assuming.
- Contexts may already exist. Hosted sessions often pre-create a context with a persistent profile. Calling
newContext()blindly can leave you driving a page nobody is watching.
If you're connecting to a raw Chrome instance rather than a managed session, you need the browser started with --remote-debugging-port and the endpoint exposed. That's the same protocol, just without the session management layer.
What you're actually downloading (and what you aren't)
This is the part that trips people up. In a remote setup:
- You still install the `playwright` npm package. It's the client. It contains the protocol bindings, not the browser.
- You do not run `playwright install`. No browser binaries land on the machine.
- You do download a connection URL. That's the artifact — a CDP WebSocket endpoint, usually scoped to a single session with a TTL.
- You may download a session token or API key. Auth for provisioning sessions, separate from the CDP URL itself.
So the "download" is configuration, not binaries. If you're building an agent or a CI job, that means your Docker image shrinks, your cold-start time drops, and browser version drift stops being a class of bug you own.
For a deeper look at how hosted sessions fit into agent architectures, see Remote Browser for AI Agents.
Production criteria before you commit
Remote browsers are infrastructure. Evaluate them like infrastructure, not like a library.
Session lifecycle. How is a session created, how long does it live, and what happens when the client disconnects? You want explicit control over teardown, not a garbage collector you can't observe.
Profile persistence. Agents that log in once and reuse a session need persistent profiles. Test that cookies and localStorage survive across sessions before you build on it.
Network controls. Proxy routing, region selection, and configurable browser settings matter for anything touching geo-restricted or bot-sensitive sites. Ask what's configurable rather than assuming.
Observability. A live viewer and session replay turn "the agent failed" into "here's the frame where it failed." Without that, remote debugging is worse than local.
Concurrency and quotas. Know your session limits and how they're metered. Remote Browser bills by browser-hour; current rates and limits are on /pricing.
Isolation. Each session should be isolated — separate profile, separate cookies, separate network identity. Shared state across sessions is a correctness bug waiting to happen.
If a provider can't answer these clearly, the demo will look great and production will not.
Common mistakes when moving to remote browsers
Assuming `connectOverCDP` works for all browsers. It doesn't. Chromium only. Plan your matrix accordingly.
Leaving sessions open. A crashed client that never closes its session burns browser-hours. Wrap session use in try/finally and set a TTL on the server side as a backstop.
Ignoring the pre-created context. Hosted sessions often hand you a context with a profile already loaded. Creating a new one silently discards that state.
Downloading browsers "just in case." If you're connecting remotely, playwright install in your Dockerfile is dead weight. Remove it and measure the image size difference.
Treating the CDP URL as a long-lived secret. Session URLs are usually short-lived and scoped. Don't cache them in a config file.
Skipping the local dev loop. Remote round-trips are slower than local ones. Keep a local Chromium install for writing and debugging tests, and switch to remote for CI and agent runs.
When remote is the right call
Remote browsers earn their place in a few specific situations:
- CI fleets where per-job browser downloads dominate build time.
- AI agents that need a browser per task without managing a browser fleet.
- Geo-distributed testing where the browser's network location is part of the test.
- Ephemeral environments — serverless functions, short-lived containers — where installing a browser isn't practical.
- Shared debugging where a teammate needs to watch a live session.
They're the wrong call when you need sub-100ms iteration, when you're testing browser-specific rendering that depends on the local OS, or when your workload is small enough that a cached local install is simply faster.
For a broader comparison of hosted versus self-managed setups, see Remote Web Browser and Remote Control Browser.
A practical migration path
If you're moving an existing Playwright suite to remote browsers, do it incrementally:
- Abstract browser acquisition. Put
launch()andconnectOverCDP()behind one function so the rest of your code doesn't care which path is active. - Add a remote path behind a flag.
REMOTE_BROWSER_CDP_URLset means remote; unset means local. No code forks. - Run both in CI for a week. Compare flake rates and durations before you commit.
- Remove `playwright install` from the remote path only. Keep it for the local path.
- Instrument session lifecycle. Log session creation, connection, and teardown. You'll find leaks fast.
The abstraction is the important part. Once getBrowser() is a single function, switching runtimes is a config change, not a refactor.
Where Remote Browser fits
Remote Browser provides hosted Chromium sessions with CDP access, Playwright/Puppeteer/Selenium compatibility, a live viewer, persistent profiles, session isolation, and configurable browser settings. You provision a session, get a CDP URL, and connect with the code above — no browser binaries on your machine.
It's built for the workloads where local installs stop scaling: agent fleets, CI at volume, and anything that needs a browser in a place your code isn't. Setup details are in the documentation, and current usage rates are on /pricing.
If you just want to see a hosted Chromium session running without installing anything, start with Remote Browser Online.
The download you were looking for isn't a binary. It's a connection string — and once you've made that shift, the browser version drift, the CI image bloat, and the "works on my machine" class of bugs go away with it.