← Blog

BLOG

Playwright ConnectOverCDP Android: A Practical Guide

Use Playwright connectOverCDP with Android Chrome over CDP. Covers adb port forwarding, remote browser endpoints, and production trade-offs.

September 18, 20268 min readRemote Browser

# Playwright ConnectOverCDP Android: A Practical Guide

playwright connectovercdp android is a search that usually means one of two things: you want to drive Chrome on a physical Android device from a desktop Playwright script, or you want to attach Playwright to a remote Chromium instance that happens to be running on Android hardware. Both are possible. Neither is as clean as the desktop CDP story, and the failure modes are specific enough that it's worth understanding what connectOverCDP actually does before you wire it up.

This guide covers the mechanics: how Android exposes Chrome's DevTools socket, how to forward it with adb, how to point Playwright at it, and where a hosted Chromium runtime fits when you don't want to manage devices at all.

What connectOverCDP Actually Does

browserType.connectOverCDP(endpointURL) opens a WebSocket connection to a Chrome DevTools Protocol endpoint and returns a Browser object. Unlike launch(), it does not start a browser process. It attaches to one that is already running and already listening on a CDP port.

That distinction matters for Android. On desktop, you start Chrome with --remote-debugging-port=9222 and connect to http://localhost:9222. On Android, Chrome does not expose a debugging port on the device's network interface by default. You have to bridge it.

The Playwright docs are explicit that connectOverCDP is Chromium-only. Firefox and WebKit do not implement the CDP surface Playwright expects, so if your Android target is Firefox for Android, this path is closed. See the Playwright CDP documentation for the canonical signature and the Chromium-only caveat.

The Android CDP Path: adb Forward

Android's Chrome exposes a Unix domain socket at @chrome_devtools_remote when remote debugging is enabled. You enable it via chrome://inspect on desktop with USB debugging on, or programmatically through adb.

The bridge is adb forward:

# List devices
adb devices

# Forward a local TCP port to Chrome's DevTools socket on the device
adb forward tcp:9222 localabstract:chrome_devtools_remote

# Verify the endpoint responds
curl http://localhost:9222/json/version

If curl returns a JSON blob with webSocketDebuggerUrl, you have a working CDP endpoint on localhost:9222. If it hangs or returns nothing, Chrome is not running with debugging enabled, or the socket name is wrong. Some OEM builds use chrome_devtools_remote without the @ prefix in the abstract namespace; adb forward handles both via localabstract:.

Once the forward is live, Playwright connects the same way it would to any desktop Chrome:

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

async function connectToAndroid(): Promise<void> {
  const browser: Browser = await chromium.connectOverCDP(
    'http://localhost:9222',
    { timeout: 30_000 }
  );

  // connectOverCDP returns existing contexts; it does not create one.
  const contexts: BrowserContext[] = browser.contexts();
  const context: BrowserContext = contexts[0] ?? await browser.newContext();
  const pages: Page[] = context.pages();
  const page: Page = pages[0] ?? await context.newPage();

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

  // Do not call browser.close() unless you want to kill the device's Chrome.
  await browser.close();
}

connectToAndroid().catch((err) => {
  console.error('CDP connect failed:', err);
  process.exit(1);
});

Two details trip people up here. First, connectOverCDP returns the contexts that already exist in the browser — it does not give you a fresh isolated context the way launch() does. If Chrome on the device has one tab open, you get one context with one page. Second, browser.close() on a CDP connection disconnects Playwright but the semantics around whether it terminates the remote browser have varied across versions. Treat it as a disconnect, not a kill, and manage the device's Chrome lifecycle separately.

Why Android CDP Is Fiddly in Practice

The adb forward approach works for a single device on a USB cable. It degrades quickly when you scale.

ConstraintDesktop ChromeAndroid Chrome via adb
Endpoint stabilityFixed local portBreaks on USB disconnect, device sleep, adb server restart
ConcurrencyMany browsers per hostOne device per USB port (or Wi-Fi adb, which is flakier)
Context isolationnewContext() per taskShared contexts; you get what Chrome already has
Version controlPin Chromium buildTied to the device's installed Chrome
HeadlessSupportedNot on stock Android Chrome
CI integrationStraightforwardRequires physical hardware or emulator farm
Profile persistence--user-data-dirDevice-managed, harder to reset cleanly

The concurrency row is the killer for agent workloads. If you're running browser-use style tasks that each need an isolated session, a single Android device is a single session. You can open multiple tabs, but they share cookies, storage, and the same browser process. That is not isolation.

Wi-Fi debugging (adb tcpip 5555) removes the cable but adds network flakiness and a security surface you probably don't want in production. Emulators solve the hardware problem but introduce their own CDP quirks — the DevTools socket name can differ, and GPU-accelerated rendering paths behave differently from physical devices.

When You Actually Need Android

There are legitimate reasons to target Android Chrome specifically:

  • Mobile-specific rendering. Responsive layouts, touch event handling, and viewport behavior differ from desktop Chromium. If your automation asserts on visual output, you need the real thing.
  • Mobile user-agent and client hints. Sites that branch on Sec-CH-UA-Mobile or serve different markup to Android will behave differently.
  • App-embedded WebViews. If you're testing a WebView inside an Android app, you can attach to it via adb and the WebView's own DevTools socket, though Playwright's support here is limited compared to Chrome proper.

If none of those apply — if you just need a Chromium instance that runs somewhere other than your laptop — Android is the wrong tool. You want a hosted Chromium runtime with a stable CDP endpoint, which is a different problem with a much cleaner solution.

The Remote Endpoint Alternative

A hosted browser runtime gives you a ws:// or wss:// CDP endpoint that behaves like a desktop Chrome with --remote-debugging-port set, minus the process management. You connect with the same connectOverCDP call:

import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP(
  process.env.REMOTE_BROWSER_WS_ENDPOINT!,
  { timeout: 30_000 }
);

const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
// ... your automation ...
await browser.close();

The difference from the Android path is that you get newContext() isolation, the endpoint survives your laptop sleeping, and you can run N of these in parallel without N USB cables. Remote Browser exposes hosted Chromium sessions over CDP with Playwright, Puppeteer, and Selenium compatibility, plus a live viewer for debugging and persistent profiles when you need session state to survive across runs. The remote browser for AI agents post covers the runtime model in more depth.

If you specifically need Android rendering, a hosted desktop Chromium will not substitute — the user agent and rendering engine differ. Be honest about which problem you have.

Puppeteer and the Same Endpoint

The CDP endpoint is not Playwright-specific. Puppeteer's connect() takes the same WebSocket URL:

const browser = await puppeteer.connect({
  browserWSEndpoint: process.env.REMOTE_BROWSER_WS_ENDPOINT,
  defaultViewport: null,
});

This is the browserWSEndpoint pattern you'll see referenced across Puppeteer docs and examples. The endpoint format is identical whether it points at a local Chrome, an Android device behind adb forward, or a hosted runtime. That portability is the point of CDP — the transport is standardized, so the client library is a choice rather than a constraint.

One caveat: connectOverCDP and Puppeteer's connect both assume the remote browser is already running. Neither will start one for you. If your endpoint is down, you get a connection error, not a fallback launch. Build health checks around the endpoint if you're running this in CI.

Production Criteria

If you're deciding between self-managed Android devices and a hosted runtime, the criteria that actually matter are:

  1. Session isolation. Can two concurrent tasks share a browser without leaking cookies or storage? Android via adb cannot. Hosted runtimes with per-session contexts can.
  2. Endpoint durability. Does the CDP URL survive a worker restart, a network blip, a deploy? adb forward does not. A managed endpoint does.
  3. Debugging surface. When a task fails at step 14, can you see what the page looked like? A live viewer or session recording beats re-running with --headed on a device you have to physically hold.
  4. Cost model. Per-device hardware has a fixed cost floor. Per-browser-hour metering scales to zero. See /pricing for current rates rather than guessing.
  5. Version pinning. Can you reproduce a failure against the exact Chromium build that produced it? Device-managed Chrome updates silently.

For a single developer debugging a mobile layout, adb forward plus connectOverCDP is fine and free. For anything running unattended, the operational overhead of physical devices usually outweighs the rendering fidelity you gain.

Common Failure Modes

`connectOverCDP` hangs. Usually the endpoint is reachable but not speaking CDP. curl http://localhost:9222/json/version should return JSON. If it returns HTML, you've hit a proxy or a different service on that port.

`browser.contexts()` is empty. Chrome is running but has no tabs. Open one, or call newContext() — though on a CDP connection to a real browser, newContext() creates an incognito-style context that may not match what you expect.

Sessions bleed between runs. You're reusing the same browser process. Either close and relaunch, or use a runtime that gives you fresh contexts per session.

Timeouts on `page.goto`. Mobile networks are slower and less predictable than your CI runner's connection. Raise the navigation timeout rather than assuming the page is broken.

`adb forward` silently drops. The forward is tied to the adb server. If the server restarts, the forward is gone and your endpoint is dead. Re-run the forward in your setup script, and check adb devices before connecting.

Where This Leaves You

playwright connectovercdp android is a solvable problem for single-device debugging. The adb forward bridge plus a connectOverCDP call gets you driving real Android Chrome from a desktop script in about five minutes. The trouble starts when you need more than one device, more than one session, or an endpoint that stays up while you sleep.

If your requirement is Android rendering specifically, accept the device management cost and script around it. If your requirement is a reliable Chromium endpoint for automation or agents, skip the Android detour and connect to a hosted runtime instead. The Playwright code is nearly identical — the difference is everything around it.

For the broader runtime model, start with /documentation and the remote browser online overview.