← Blog

BLOG

Playwright Browser Launch Options: A Production Guide

Playwright browser launch options control how Chromium starts. Learn the flags that matter, plus how to launch remote browsers via CDP.

September 12, 202610 min readRemote Browser

# Playwright Browser Launch Options: A Production Guide

Playwright browser launch options are the arguments you pass to chromium.launch(), firefox.launch(), or webkit.launch() to control how the browser process starts. They determine headless mode, executable path, proxy settings, viewport, permissions, and a long list of Chromium flags. Get them wrong and you get flaky tests, slow startups, or automation that gets blocked. Get them right and your scripts run predictably across local machines, CI runners, and hosted runtimes.

This guide covers the Playwright browser launch options that actually matter in production, the trade-offs between local and remote execution, and how to move from launch() to connectOverCDP() when you need a browser that outlives your script.

What Playwright Browser Launch Options Actually Control

When you call chromium.launch(), Playwright spawns a browser process and passes your options to it. The options fall into a few categories:

  • Process controlheadless, executablePath, channel, args, env, timeout
  • Networkproxy, ignoreHTTPSErrors
  • Renderingviewport, deviceScaleFactor, colorScheme, locale, timezoneId
  • Permissions and mediapermissions, geolocation, recordVideo, recordHar
  • Debuggingdevtools, slowMo, trace

Most teams start with a handful of these and add more as problems appear. The pattern is predictable: something breaks in CI, someone adds a flag, and the launch config grows into an undocumented pile of workarounds.

The better approach is to understand which Playwright browser launch options are load-bearing and which are noise.

Options That Change Behavior

headless is the most consequential. Headless Chromium is faster and cheaper, but some sites detect it. Playwright's default headless mode uses the new headless implementation (the same Chromium binary, no separate headless_shell), which is closer to headed behavior than the old headless mode. If you're fighting detection, headless: false with a virtual display is sometimes the only fix — but that's a signal you should be running on a hosted browser with a real rendering stack instead.

executablePath and channel determine which browser binary runs. channel: 'chrome' uses your installed Google Chrome; channel: 'msedge' uses Edge. This matters because Playwright's bundled Chromium and branded Chrome have different codec support, different default flags, and different update cadences. If your automation depends on DRM, proprietary codecs, or Chrome-specific behavior, channel is not optional.

proxy routes all browser traffic through a proxy server. This is where most production setups diverge from local development. A single proxy works for a single script; a fleet of agents needs per-session proxies, rotation, and geographic targeting. Playwright's proxy option handles the first case cleanly and the second case poorly.

args is the escape hatch. Anything Chromium accepts on the command line can go here. Common entries:

  • --disable-blink-features=AutomationControlled — removes the navigator.webdriver signal
  • --disable-dev-shm-usage — avoids /dev/shm exhaustion in containers
  • --no-sandbox — required in many Docker images, but weakens isolation
  • --disable-gpu — avoids GPU init failures on headless servers
  • --window-size=1920,1080 — sets the window before the first paint

Each of these has a cost. --no-sandbox is a real security trade-off. --disable-blink-features=AutomationControlled is a detection arms race you can't win from the client side alone.

Local Launch vs Remote Launch: The Real Trade-off

The Playwright browser launch options you need depend on where the browser runs. Here's how the two models compare:

ConcernLocal launch()Remote connectOverCDP()
Browser binaryYou install and update itRuntime manages it
Startup time1–3s per launchConnection only, browser already running
Session persistenceDies with the scriptSurvives script restarts
Proxy managementPer-launch configPer-session, runtime-managed
ScalingOne process per scriptMany sessions per runtime
DebuggingLocal DevToolsLive viewer + CDP
Resource limitsYour machine's RAM/CPURuntime-enforced
Cost modelYour infraPer browser-hour

Local launch() is the right choice for development, one-off scripts, and test suites that run on a single machine. It's simple, debuggable, and free.

Remote connectOverCDP() is the right choice when you need browsers that outlive your script, run from a stable IP, or scale beyond what one machine can hold. The launch options move from your code to the runtime's configuration, and your script becomes a client instead of a process manager.

The mistake teams make is treating this as a binary. In practice, you develop locally with launch() and deploy with connectOverCDP(), keeping the same Playwright API surface. The remote browser for AI agents guide covers the runtime side of that split in more detail.

Installing and Managing Playwright Browsers

Before any launch option matters, the browser has to exist. Playwright ships its own browser binaries, and managing them is a recurring source of friction.

# Install all supported browsers
npx playwright install

# Install only Chromium
npx playwright install chromium

# Install with system dependencies (Linux CI)
npx playwright install --with-deps chromium

# Remove browsers to reclaim disk
npx playwright uninstall

Playwright's supported browsers are Chromium, Firefox, and WebKit. Each has its own build, and each build is pinned to a Playwright version. Upgrading Playwright often requires reinstalling browsers, which is why playwright install shows up in so many CI configs.

Manual installation is possible — you can point executablePath at any compatible binary — but you lose the version pinning that makes Playwright reproducible. If you're running in a container, --with-deps handles the system libraries that headless Chromium needs (libnss3, libatk, libgbm, and friends). Miss one and you get a cryptic launch failure.

The disk cost is real. A full playwright install pulls roughly 1GB across the three browsers. In ephemeral CI runners, that's a per-job download unless you cache it. In a hosted runtime, it's the runtime's problem, not yours.

Launching a Remote Browser with connectOverCDP

When you connect to a remote browser, you skip launch() entirely. The runtime has already started Chromium and exposed a CDP endpoint. Your script connects to it.

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

interface RemoteSession {
  cdpUrl: string;
  sessionId: string;
}

async function connectToRemoteBrowser(session: RemoteSession): Promise<{
  browser: Browser;
  context: BrowserContext;
  page: Page;
}> {
  // Connect to the remote Chromium instance over CDP.
  // The runtime returns a WebSocket URL like:
  // wss://<host>/cdp/<session-id>
  const browser = await chromium.connectOverCDP(session.cdpUrl, {
    timeout: 30_000,
    // Slow down operations for debugging; omit in production.
    slowMo: 0,
  });

  // A remote session typically exposes one persistent context.
  // Reuse it rather than creating a new one, or you lose
  // cookies, localStorage, and any authenticated state.
  const contexts = browser.contexts();
  const context = contexts.length > 0
    ? contexts[0]
    : await browser.newContext({
        viewport: { width: 1440, height: 900 },
        locale: 'en-US',
        timezoneId: 'America/New_York',
      });

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

  // Set a sane default so a hung navigation fails fast.
  page.setDefaultTimeout(20_000);
  page.setDefaultNavigationTimeout(45_000);

  return { browser, context, page };
}

async function runTask(cdpUrl: string, sessionId: string) {
  const { browser, page } = await connectToRemoteBrowser({ cdpUrl, sessionId });

  try {
    await page.goto('https://example.com/dashboard', {
      waitUntil: 'domcontentloaded',
    });
    await page.getByRole('button', { name: 'Export' }).click();
    await page.waitForEvent('download');
  } finally {
    // Close the connection, not the browser.
    // The runtime keeps the session alive for reuse.
    await browser.close();
  }
}

Two details matter here. First, browser.close() on a CDP connection disconnects your client; it does not necessarily terminate the remote browser. That's the point — the session persists. Second, browser.contexts() returns the existing contexts. Creating a new context on every connection throws away the state that makes persistent profiles useful.

If you're migrating from launch() to connectOverCDP(), the API surface is nearly identical. The differences are in lifecycle: you don't own the browser process, so you can't pass args or executablePath. Those become runtime configuration instead. The Playwright CDP connection guide walks through the connection model in more depth.

What You Give Up When You Move Launch Options to the Runtime

Moving from local launch() to a hosted runtime is not free. You trade control for reliability, and the trade is worth it only if you understand what you're giving up.

You lose per-launch flag control. args, executablePath, and channel are set by the runtime. If you need a specific Chromium flag for a specific site, you either configure it at the session level (if the runtime supports it) or you're out of luck. Remote Browser exposes configurable browser settings per session, but the exact set is documented at /documentation rather than assumed.

You lose the local DevTools workflow. Debugging a remote browser means using the runtime's live viewer or attaching your own CDP client. This is usually fine — a live viewer is often better than a local DevTools window — but it's a different muscle.

You gain session persistence. A local launch() dies when the script ends. A remote session survives, which means you can authenticate once and reuse the profile across many tasks. For agents that log into the same site repeatedly, this is the difference between a working system and a CAPTCHA farm.

You gain stable networking. Local browsers run from your IP. Remote browsers run from the runtime's IP, which can be a datacenter range or a residential proxy depending on configuration. If your target site blocks datacenter IPs, this matters more than any launch flag.

You gain metering. Remote sessions are billed per browser-hour. That's a real cost that local execution doesn't have. Current rates are at /pricing; the short version is that you pay for time, not for concurrency, so idle sessions are the thing to watch.

Production Criteria for Choosing Launch Options

If you're deciding which Playwright browser launch options to configure, work backward from failure modes.

If your scripts fail in CI but pass locally, the problem is usually --disable-dev-shm-usage, missing system deps, or a viewport mismatch. Fix those before adding stealth flags.

If your scripts get blocked, the problem is rarely a launch option. It's IP reputation, request patterns, or missing browser state. --disable-blink-features=AutomationControlled helps at the margin; a residential proxy and a persistent profile help more.

If your scripts are slow, the problem is usually browser startup. Each launch() costs 1–3 seconds. If you're launching per task, you're paying that cost on every run. A persistent remote session amortizes it to zero.

If your scripts leak state between runs, you need session isolation. Local launch() gives you a fresh browser every time, which is clean but slow. Remote sessions give you isolation at the session level, so you can have both persistence and cleanliness — just not in the same session.

If your scripts need to run 24/7, local execution is the wrong model. You need a runtime that keeps browsers alive across script restarts, handles reconnection, and meters usage. That's the case for a hosted runtime, and it's covered in the remote browser online guide.

A Practical Migration Path

Start local. Use launch() with a minimal config:

const browser = await chromium.launch({
  headless: true,
  args: ['--disable-dev-shm-usage'],
});

Add options only when you hit a specific problem. Keep a comment explaining why each flag exists — future you will not remember.

When you outgrow local execution — because you need persistence, scale, or stable networking — switch to connectOverCDP(). Keep your page-level code identical. Move the launch configuration into session setup, and let the runtime own the browser process.

The Playwright API is designed for this. The same page.getByRole(), page.waitForEvent(), and context.storageState() calls work whether the browser is on your laptop or in a datacenter. The Playwright browser launch options are the only part that changes, and they change less than you'd expect.

For the full picture of how hosted Chromium sessions, persistent profiles, and CDP access fit together, see the remote web browser overview. For the API surface, start at /documentation.

The short version: Playwright browser launch options are a local concern. Once you're in production, the browser is infrastructure, and infrastructure is configured once, not per script.