BLOG
Playwright LaunchOptions Args: A Production Guide
Playwright launchOptions args control Chromium at startup. Learn which flags matter, which break CDP, and how to run them on a remote browser.
# Playwright LaunchOptions Args: A Production Guide
playwright launchoptions args is the array you pass to chromium.launch({ args: [...] }) to change how Chromium starts. Those flags decide whether your browser runs headless, whether it exposes a debugging port, how it handles sandboxing, and whether it can be reached over CDP from another process. Get them wrong and you get silent failures: a browser that launches but never connects, a container that crashes under load, or a session that leaks memory across many runs.
This guide covers what the args array actually does, which flags are worth setting in production, which ones conflict with remote connections, and how the same configuration maps onto a hosted Chromium runtime instead of a local binary.
What launchOptions args actually control
When Playwright calls chromium.launch(), it spawns a Chromium process and passes your args entries directly to the command line. Playwright adds its own defaults on top — a temporary user data directory, --remote-debugging-pipe for the default transport, and a set of flags that disable first-run prompts and background services.
The args array is therefore a startup contract. It is evaluated once, before any page exists. You cannot change most of these flags mid-session, and a flag that conflicts with Playwright's own defaults can prevent the browser from starting at all.
The most common entries fall into four groups:
- Transport and debugging —
--remote-debugging-port,--remote-debugging-address. These matter when something other than Playwright needs to attach. - Rendering and display —
--headless,--disable-gpu,--window-size. These affect layout and screenshot output. - Sandbox and process isolation —
--no-sandbox,--disable-dev-shm-usage,--disable-setuid-sandbox. These are container workarounds, not optimizations. - Feature toggles —
--disable-extensions,--disable-background-networking,--disable-features=.... These reduce noise and startup cost.
Everything else — proxy settings, user agent, viewport, permissions — belongs in launchOptions fields or contextOptions, not in args. Mixing the two is a common source of confusion.
The flags that matter in production
--no-sandbox and its relatives
Running Chromium as root inside a container without a user namespace fails unless you pass --no-sandbox. This is the single most common reason a launch works locally and dies in CI.
The trade-off is real: --no-sandbox removes the renderer sandbox, which is your primary defense against a compromised page escaping into the host. If you control the pages being loaded, the risk is bounded. If you are loading arbitrary URLs from the open web, it is not.
The better fix is to run the browser as a non-root user with a properly configured seccomp profile, which is what managed runtimes do by default. --disable-dev-shm-usage is a separate issue: Docker's default /dev/shm is 64 MB, and Chromium will crash on memory-heavy pages without it. That flag is safe and worth keeping.
--headless and the new headless mode
Playwright's headless: true option already handles this. Passing --headless in args as well is redundant and, with newer Chromium builds, can conflict with the --headless=new variant Playwright selects. Set headless as a launch option and leave it out of args.
If you need the full browser rendering path — for canvas, WebGL, or sites that detect headless — you want a headed browser on a virtual display, not a flag. That is a runtime concern, not a launch argument.
--remote-debugging-port
This flag is what makes a browser reachable over CDP from outside the process that launched it. It is also the flag most likely to be misused.
By default Playwright connects to its own browser over a pipe, not a port. Adding --remote-debugging-port=9222 opens a second transport. If you are launching locally and connecting locally, you do not need it. If you are launching a browser that another process — an agent, a test runner, a different machine — will attach to, you do.
Two things to know:
- The port binds to
127.0.0.1unless you also pass--remote-debugging-address=0.0.0.0. Exposing that port to a network without authentication is a remote code execution vector. Do not do it on a shared host. - Playwright's
connectOverCDP()expects a browser that was started with a debugging endpoint. A browser launched bychromium.launch()with default options does not expose one.
This is the point where local launch arguments stop being the right abstraction. Once you need a browser that outlives the process that started it, you are describing a remote browser.
Local launch vs. remote connection: what changes
| Concern | chromium.launch({ args }) | chromium.connectOverCDP(endpoint) |
|---|---|---|
| Who starts Chromium | Your process | The remote runtime |
| Where args are set | Your code | Runtime configuration |
| Browser lifetime | Tied to your process | Independent, survives restarts |
| Debugging port | You manage it | Provided by the runtime |
| Sandbox / seccomp | Your responsibility | Handled by the host |
| Profile persistence | Manual userDataDir | Managed persistent profiles |
| Scaling | One process per browser | Sessions provisioned on demand |
| Failure mode | Process crash kills session | Session survives worker restart |
The practical consequence: if your args array is mostly --no-sandbox, --disable-dev-shm-usage, and --remote-debugging-port, you are hand-rolling infrastructure that a hosted runtime already provides. The flags are not wrong — they are just solving a problem you can move elsewhere.
Connecting Playwright to a remote browser
The connection path is short. You take an endpoint URL from the runtime and hand it to connectOverCDP. No local Chromium binary, no launch args, no version drift between the browser you tested against and the browser that runs in production.
import { chromium, Browser, BrowserContext, Page } from 'playwright';
const WS_ENDPOINT = process.env.BROWSER_WS_ENDPOINT;
if (!WS_ENDPOINT) {
throw new Error('BROWSER_WS_ENDPOINT is not set');
}
async function runTask(): Promise<void> {
const browser: Browser = await chromium.connectOverCDP(WS_ENDPOINT, {
timeout: 30_000,
});
// A remote session usually arrives with a context already open.
const context: BrowserContext =
browser.contexts()[0] ?? (await browser.newContext());
const page: Page = await context.newPage();
try {
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 45_000,
});
const title = await page.title();
console.log('loaded:', title);
} finally {
await page.close();
// Do not call browser.close() on a shared remote session unless
// you intend to terminate it. Disconnect instead.
await browser.close();
}
}
runTask().catch((err) => {
console.error(err);
process.exit(1);
});Two details that trip people up. First, browser.contexts() on a CDP connection returns the contexts the remote browser already has — creating a new one is often unnecessary and can fragment your profile state. Second, browser.close() on a CDP connection disconnects Playwright; whether it terminates the remote browser depends on the runtime. Check the behavior of your provider before relying on it for cleanup.
For the full connection surface — including connect() over the Playwright protocol rather than raw CDP — see the documentation.
When you still need launch args
Remote connection does not eliminate args entirely. There are cases where you genuinely need to control browser startup:
- Extensions. Loading an unpacked extension requires
--load-extensionand--disable-extensions-exceptat launch. This is one of the few things you cannot do over a plain CDP connection to an already-running browser. - Feature flags for testing. If you are verifying that your app behaves correctly with a specific Chromium feature disabled, you need
--disable-featuresat startup. - Custom user agent at the process level. Rare, but some enterprise proxies inspect the process-level UA string.
For these, the right pattern is to configure the browser at the runtime level rather than in your application code. A hosted runtime that supports configurable browser settings lets you specify startup flags per session, so the flags live with the infrastructure instead of being copy-pasted into every script.
If you are currently maintaining a local launch configuration and want to understand the migration path, Remote Browser for AI agents covers the runtime model in more detail.
Puppeteer, Selenium, and the same problem
The args array is not a Playwright-specific concept. Puppeteer's puppeteer.launch({ args }) and Selenium's ChromeOptions.addArguments() take the same flags, because all three are ultimately configuring the same Chromium command line.
This matters for two reasons.
First, the flags are portable. A --disable-dev-shm-usage that fixes a Puppeteer crash in Docker fixes the same crash in Playwright. If you are migrating between frameworks, your launch configuration usually moves with you.
Second, the connection story is converging. Puppeteer's puppeteer.connect({ browserWSEndpoint }) and Playwright's connectOverCDP() both speak CDP to a browser that was started elsewhere. A runtime that exposes a WebSocket endpoint works with either. If you are evaluating providers, check whether they expose a raw CDP endpoint rather than a framework-specific SDK — it keeps your options open.
The Chrome DevTools Protocol documentation is the authoritative reference for what that endpoint actually exposes. Worth reading before you build tooling on top of it.
Production criteria for launch configuration
If you are deciding whether to keep managing launch args yourself or move to a hosted runtime, these are the questions that actually matter:
- Does the browser survive a worker restart? If your automation runs in a serverless function or a spot instance, a locally launched browser dies with the process. A remote session does not.
- Can you reproduce the exact browser version? Local installs drift.
npx playwright installpulls whatever the current Playwright version pins. A hosted runtime gives you a consistent build. - How do you handle profiles? Persistent profiles — cookies, localStorage, logged-in state — require a stable
userDataDir. Managing that across ephemeral containers is its own project. - What is your sandbox story? If the answer is
--no-sandboxand a hope, that is a finding. - How do you debug a failed run? A live viewer that lets you watch the session in real time is worth more than a stack trace and a screenshot.
None of these are launch-argument problems. They are runtime problems that launch arguments happen to touch.
A note on flag sprawl
There is a genre of launch configuration that accumulates flags over years, each one added to fix a specific incident, none of them documented. --disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints and dozens of others.
This is a maintenance liability. Every flag is a behavior change you are now responsible for testing against. Some of them — particularly --disable-features entries — silently change rendering or network behavior in ways that only surface on specific sites.
The discipline that works: keep a minimal set of flags you can justify, document why each one exists, and re-evaluate them when you upgrade Chromium. If a flag was added to work around a container limitation, moving to a managed runtime lets you delete it.
Where remote-browser.dev fits
Remote Browser provides hosted Chromium sessions with CDP access, so Playwright, Puppeteer, and Selenium connect the same way they would to a local browser — minus the launch configuration. Sessions support persistent profiles, configurable browser settings, session isolation, and a live viewer for debugging. Usage is metered; see pricing for current details.
The pattern that works well: keep chromium.launch() for local development and fast iteration, where you control the machine and the flags are cheap. Switch to connectOverCDP() against a hosted endpoint for anything that needs to survive a restart, run at concurrency, or reproduce a specific browser build.
If you want to see the connection working before committing to anything, Remote Browser online walks through getting a session endpoint and driving it from a script. And if you are still deciding between local and hosted, remote web browser covers the trade-offs without the sales pitch.
The short version: launchOptions.args is a startup contract for a browser process you own. The moment you stop owning that process — because it needs to outlive your code, scale past one machine, or match a build you did not install — the args array stops being the right place to solve the problem.