> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tilion.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect multiple CDP clients

> Attach a main client and up to two observers to the same browser session.

When multiple CDP clients are enabled for your account, a session supports one main client and up to two observers. Use the session's `connect_url` for the main client. Add `&role=observer` for each observer.

Observers can send CDP commands and receive events. They share the same browser and can change its pages, so coordinate actions between clients. The observer role is not read-only.

## Attach two clients

Start with a [running session](https://docs.tilion.dev/guides/create-browser.md) and set `TILION_API_KEY` and `TILION_SESSION_ID`. Install `requests` and `playwright` for Python, or `playwright-core` for Node.js. No local browser installation is required.

These examples connect a main client and an observer, open a page through the main client, and read its title through the observer. Run the Node.js example as an `.mjs` file with Node 20 or later.

```python Python theme={"dark"}
import os
import requests
from playwright.sync_api import sync_playwright

sid = os.environ["TILION_SESSION_ID"]
response = requests.get(
    f"https://back.tilion.dev/v1/session/{sid}/connect",
    headers={"Authorization": f"Bearer {os.environ['TILION_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
connect_url = response.json()["connect_url"]

with sync_playwright() as p:
    main = p.chromium.connect_over_cdp(connect_url, timeout=60000)
    try:
        observer = p.chromium.connect_over_cdp(
            connect_url + "&role=observer", timeout=60000
        )
        try:
            page = main.contexts[0].pages[0]
            page.goto("https://example.com/", wait_until="domcontentloaded")
            observed_page = next(
                page for page in observer.contexts[0].pages
                if page.url == "https://example.com/"
            )
            print(observed_page.title())
        finally:
            observer.close()
    finally:
        main.close()
```

```javascript Node.js theme={"dark"}
import { chromium } from "playwright-core";

const sid = process.env.TILION_SESSION_ID;
const response = await fetch(`https://back.tilion.dev/v1/session/${sid}/connect`, {
  headers: { Authorization: `Bearer ${process.env.TILION_API_KEY}` },
  signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(await response.text());
const { connect_url } = await response.json();

const main = await chromium.connectOverCDP(connect_url, { timeout: 60_000 });
try {
  const observer = await chromium.connectOverCDP(
    `${connect_url}&role=observer`, { timeout: 60_000 },
  );
  try {
    const page = main.contexts()[0].pages()[0];
    await page.goto("https://example.com/", { waitUntil: "domcontentloaded" });
    const observedPage = observer.contexts()[0].pages()
      .find(page => page.url() === "https://example.com/");
    if (!observedPage) throw new Error("Page not found in observer context");
    console.log(await observedPage.title());
  } finally {
    await observer.close();
  }
} finally {
  await main.close();
}
```

The examples print `Example Domain` and disconnect both Playwright clients. The session remains running. Delete it with `DELETE /v1/session/{id}` when finished.

## Add a second observer

Another process can connect to the same URL with `&role=observer`, including a raw CDP recorder. Clients can connect and disconnect in any order. Observers can connect before the main client and stay connected after it leaves.

Main-client navigation receives automatic recovery when `cdp_observe: true`, the default. Setting it to `false` disables that behavior. Observer commands go directly to the browser without that recovery. On a [custom-proxy session](https://docs.tilion.dev/guides/custom-proxy.md), recovery never switches the proxy exit.

## Send raw CDP commands from an observer

Address targets with `sessionId`, as Playwright and Puppeteer already do. Observers cannot use `Target.sendMessageToTarget` or `Target.exposeDevToolsProtocol`, which can carry another command. The browser-wide commands `Browser.close`, `Browser.crash`, and `Browser.crashGpuProcess` are also refused with a CDP error, regardless of how the method name is written.

Send text frames containing one JSON object each, with no repeated keys. Other frame formats return CDP error `-32700`. See [Observer protocol restrictions](https://docs.tilion.dev/sessions/playwright-cdp.md#observer-protocol-restrictions) for the reference.

## Use REST alongside observers

A connected main client holds the session, so REST drive calls return `409 session_driven_by_client`. Disconnect the main client before using REST. Observers do not hold the session and can remain connected, but their commands can still interfere with REST actions.

Playwright's `browser.close()` disconnects a client attached through `connect_over_cdp` or `connectOverCDP`. The raw CDP command `Browser.close` closes the browser when sent by the main client and is refused for observers. Close a raw observer's WebSocket to disconnect it.

See the [CDP reference](https://docs.tilion.dev/sessions/playwright-cdp.md#multiple-cdp-clients) for client permissions and WebSocket close codes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.