← Blog

BLOG

Playwright Use Existing Browser: Connect to Live Chrome via CDP

Playwright use existing browser: connect to a live Chrome instance over CDP for debugging, AI agents, and persistent sessions. A practical guide.

September 2, 202611 min readRemote Browser

# Playwright Use Existing Browser: Connect to Live Chrome via CDP

Playwright is the de facto standard for browser automation, but its default execution model assumes you start a fresh browser instance, run a script, and tear everything down. That model breaks down when you need to debug a flaky test, attach an AI agent to a session that's already running, or scale browser workloads across cloud workers. The solution is to make Playwright use an existing browser instead of launching a new one.

This guide explains how to connect Playwright to an existing Chrome instance over the Chrome DevTools Protocol (CDP). You'll learn the practical differences between connectOverCDP and launch, when attaching to a live browser is the right architectural choice, and how to move from a local Chrome tab to a hosted Chromium runtime that survives process restarts.

Why Attach to an Existing Browser?

The standard playwright.chromium.launch() command is stateless and ephemeral. It spawns a fresh Chromium process, executes your automation, and closes it. This is fine for simple test suites, but it creates three recurring problems in production:

  1. Lost context. Cookies, local storage, and session tokens vanish when the process dies. Any automation that requires a logged-in state must re-authenticate on every run.
  2. No live debugging. You can't watch a script execute in real time or manually intervene when an AI agent gets stuck on a CAPTCHA.
  3. Scaling friction. Running many browser instances on a single machine exhausts memory and CPU. Distributing them across workers requires a connection layer that launch() doesn't provide.

Connecting to an existing browser solves all three. Instead of managing the browser lifecycle, you connect to a browser that is already running—either locally or in the cloud—and drive it over CDP.

Playwright connectOverCDP vs. launch

Playwright provides two distinct methods for attaching to an existing browser. Understanding the difference is critical.

MethodTargetUse CaseSession Persistence
playwright.chromium.launch()Starts a new browser processShort-lived, isolated testsNone—browser closes when script ends
playwright.chromium.launchPersistentContext()Starts a new browser with a user data directoryTests requiring persistent profilesYes—profile saved to disk
playwright.chromium.connectOverCDP()Connects to an already running Chrome/ChromiumDebugging, attaching to remote browsers, AI agentsDepends on the target browser's lifecycle

The key insight: connectOverCDP does not start a browser. It connects to one that is already listening on a debugging port. If that browser is a local Chrome instance you started manually, it will keep running after your Playwright script exits. If it's a hosted browser in the cloud, it remains alive until you explicitly close it or your session budget expires.

How to Connect Playwright to an Existing Browser

To use connectOverCDP, the target browser must be launched with the --remote-debugging-port flag. Here's the minimal local setup:

# Start Chrome with remote debugging enabled
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile

Once Chrome is running, you can connect to it from Playwright:

import { chromium } from 'playwright';

async function attachToExistingBrowser() {
  // Connect to the Chrome instance listening on port 9222
  const browser = await chromium.connectOverCDP('http://localhost:9222');

  // Get the existing context (or create a new one)
  const context = browser.contexts()[0] || await browser.newContext();

  // Use the existing page or open a new tab
  const page = context.pages()[0] || await context.newPage();

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

  // Do NOT close the browser here—it's the existing Chrome instance.
  // Closing it will terminate the user's browser session.
}

attachToExistingBrowser();

There are two critical differences from a standard launch() script:

  1. You don't call `browser.close()`. Closing the browser will kill the existing Chrome instance. Instead, you should call browser.disconnect() if you want to detach without terminating the browser.
  2. You must handle existing contexts and pages. A live browser may already have tabs open. Your script needs to decide whether to reuse them or create new ones.

The Localhost Problem: Why Local CDP Isn't Enough

The local connectOverCDP approach works for debugging a single script, but it has a hard ceiling. The Chrome instance is tied to your machine's process tree. If your laptop sleeps, the browser dies. If your CI worker is recycled, the session is lost. If you need to scale to many concurrent sessions, you can't run dozens of Chrome instances on a laptop.

This is where the "existing browser" needs to be a remote, hosted Chromium instance rather than a local process. The CDP connection string changes from http://localhost:9222 to a remote endpoint, but the Playwright API remains the same.

Playwright Use Existing Browser in the Cloud

When you move from a local Chrome tab to a hosted browser runtime, the architecture shifts. You are no longer responsible for keeping the browser process alive. Instead, you connect to a browser that is managed as infrastructure.

A hosted browser runtime like Remote Browser provides:

  • Persistent sessions. The browser stays alive across multiple API calls, scripts, or worker restarts.
  • Live viewer. You can watch the browser in real time via a web interface, which is essential for debugging AI agents.
  • Profile persistence. Cookies and local storage survive across sessions, so your automation stays logged in.
  • Proxy and stealth configuration. You can route traffic through specific IPs without modifying your Playwright code.

The connection pattern is identical to the local example, but the endpoint points to a hosted browser:

import { chromium } from 'playwright';

async function connectToHostedBrowser(cdpUrl: string) {
  // cdpUrl is provided by the hosted browser runtime
  const browser = await chromium.connectOverCDP(cdpUrl);

  const context = browser.contexts()[0] || await browser.newContext();
  const page = context.pages()[0] || await context.newPage();

  // Your automation logic here
  await page.goto('https://example.com');

  // Disconnect without closing the hosted browser
  await browser.disconnect();
}

The advantage is that the browser process is not tied to your script's lifecycle. You can run a Playwright script, disconnect, and reconnect later with the same session state intact.

Keeping Browser Sessions Alive Across Cloud Workers

A common production pattern is running automation across multiple cloud workers (e.g., AWS Lambda, Google Cloud Run, or Fly.io). The challenge is that these platforms are stateless by default. Each invocation may run on a different machine, and local browser processes don't survive between invocations.

If you use launch() inside a serverless function, you pay the cold start penalty on every invocation—spawning a new Chromium process takes 2–5 seconds. Worse, any session state is lost when the function returns.

The solution is to decouple the browser from the worker. Instead of launching a browser inside the worker, the worker connects to a persistent browser session hosted elsewhere. This pattern is sometimes called "browser as a service" or "remote browser."

The workflow looks like this:

  1. Provision a browser session via an API call. The runtime starts a Chromium instance and returns a CDP endpoint.
  2. Run your Playwright script in the worker, connecting to the CDP endpoint.
  3. Keep the session alive after the worker finishes. The browser remains available for the next invocation.
  4. Reconnect later with the same cookies, local storage, and open tabs.

This approach eliminates cold starts and preserves session state across worker invocations. For a deeper dive into this architecture, see our guide on remote browsers for AI agents.

CDP vs. WebDriver: Which Protocol Should You Use?

Playwright supports two connection modes for existing browsers: CDP and WebDriver (via Selenium). The choice matters for production workloads.

ProtocolPlaywright SupportBest ForLimitations
CDP (connectOverCDP)Native, first-classChrome/Chromium/Edge, debugging, performanceFirefox support is limited
WebDriver (Selenium)Via playwright-webdriverCross-browser testing (Firefox, Safari)Slower, less granular control

If you're standardizing on Chromium, CDP is the better choice. It gives you direct access to the browser's internal debugging protocol, which means lower latency and more control over network, performance, and runtime behavior.

Firefox is a different story. Playwright's official documentation notes that connectOverCDP is designed for Chromium-based browsers. Firefox does not expose a CDP endpoint in the same way. If you need Firefox, you have two options:

  1. Use Playwright's native Firefox support with launch() or launchPersistentContext().
  2. Use Selenium's WebDriver protocol, which Firefox supports natively.

For most AI agent and browser automation workloads, Chromium is the pragmatic default. It has the best CDP support, the largest market share, and the most mature tooling ecosystem.

Scaling Playwright Browser Workloads Reliably

Once you move beyond a single script, the question becomes: how do you run many Playwright sessions without managing dozens of browser processes?

The naive approach is to run multiple browser instances on a single machine. This works up to a point, but Chromium is memory-hungry. Each instance consumes 200–500 MB of RAM, and CPU contention causes flaky timing in tests.

The production approach is to treat browsers as a pool of remote resources. Instead of launching a browser per task, you acquire a browser from a pool, run your task, and release it back. This is analogous to how database connection pooling works.

A hosted browser runtime handles this pooling for you. You don't need to worry about:

  • Memory management. The runtime allocates browsers across machines based on demand.
  • Session isolation. Each task gets a clean browser profile unless you explicitly request a persistent one.
  • Geographic distribution. You can provision browsers in specific regions to reduce latency or access region-locked content.
  • Failure recovery. If a browser crashes, the runtime can provision a replacement and preserve the session state.

For a practical comparison of hosted vs. self-hosted infrastructure, see our analysis of browser-as-a-service vs. self-hosted Playwright infra.

Practical Example: AI Agent with Persistent Login

Let's put this together with a concrete example. Suppose you're building an AI agent that needs to check a dashboard daily. The agent must be logged in, but you don't want to re-authenticate every day.

Step 1: Provision a persistent browser session.

// Provision a hosted browser with a persistent profile
const response = await fetch('https://api.remote-browser.dev/v1/browsers', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
  body: JSON.stringify({
    persistent: true,
    profileName: 'dashboard-agent',
  }),
});

const { cdpUrl } = await response.json();

Step 2: Connect Playwright and authenticate once.

import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP(cdpUrl);
const context = browser.contexts()[0] || await browser.newContext();
const page = context.pages()[0] || await context.newPage();

// Authenticate (only needed on first run)
await page.goto('https://dashboard.example.com/login');
await page.fill('#username', 'agent-user');
await page.fill('#password', process.env.DASHBOARD_PASSWORD);
await page.click('#login-button');
await page.waitForURL('**/dashboard');

// The session is now saved in the persistent profile
await browser.disconnect();

Step 3: Reconnect later without re-authenticating.

// Days later, connect to the same browser session
const browser = await chromium.connectOverCDP(cdpUrl);
const context = browser.contexts()[0];
const page = context.pages()[0] || await context.newPage();

// Already logged in—no authentication needed
await page.goto('https://dashboard.example.com/daily-report');
const report = await page.textContent('#report');

This pattern works because the browser session is persistent. The cookies and local storage from the login step are stored in the profile, not in your script's memory.

Security Considerations for Remote CDP

Exposing a CDP endpoint is equivalent to giving someone full control of the browser. Anyone with access to the endpoint can read cookies, intercept network traffic, and execute arbitrary JavaScript in the page context.

When you run a local Chrome instance with --remote-debugging-port=9222, that endpoint is bound to localhost by default. This is safe as long as no other process on your machine can access it.

When you move to a hosted browser, the CDP endpoint is exposed over the network. The runtime must handle authentication and encryption. Look for these security features:

  • Per-session credentials. Each CDP endpoint should require a unique token or API key.
  • TLS encryption. The CDP connection should be encrypted in transit.
  • Network isolation. The browser should not be accessible from the public internet without authentication.
  • Session expiry. Idle sessions should be terminated after a configurable timeout.

For more details on how Remote Browser handles these concerns, see the documentation.

When Not to Use connectOverCDP

Attaching to an existing browser is not always the right choice. Here are cases where launch() is preferable:

  • Short-lived, isolated tests. If each test needs a clean browser state and runs in under a minute, launching a fresh browser is simpler and more reliable.
  • Parallel test execution. Running many tests in parallel is easier with isolated browser instances than with connections to shared browsers.
  • Cross-browser testing. If you need Firefox or WebKit, CDP is not the right protocol.

The decision comes down to state management. If your automation is stateless, use launch(). If it needs to persist state or survive process restarts, use connectOverCDP with a persistent browser.

Conclusion

Making Playwright use an existing browser is a fundamental shift in how you architect browser automation. Instead of treating the browser as a disposable process, you treat it as a persistent resource that your code connects to and disconnects from.

The connectOverCDP API makes this shift straightforward. The same Playwright code that drives a local Chrome instance can drive a hosted Chromium runtime in the cloud. The difference is in the infrastructure: who keeps the browser alive, how sessions are persisted, and how you scale beyond a single machine.

For production workloads—AI agents, long-running automation, or anything that requires persistent login state—the hosted browser model is the practical choice. It decouples your automation logic from the browser lifecycle, which is the key to building reliable, scalable browser automation.

Ready to move from local Chrome tabs to a hosted runtime? Explore Remote Browser's pricing or read more about connecting to remote browsers to see how the pieces fit together.