# How to Run 10+ Playwright Agents on One Logged-In Browser

> A multi-agent browser automation tutorial — how to share one logged-in Chromium across any number of Playwright agents for AI agent orchestration, with bonus connections for AI browsers, OpenClaw skills, and Claude Code MCP.

Content type: article
Source URL: https://www.agentpmt.com/articles/how-to-run-10-playwright-agents-on-one-logged-in-browser-without-getting-blocked
Markdown URL: https://www.agentpmt.com/articles/how-to-run-10-playwright-agents-on-one-logged-in-browser-without-getting-blocked?format=agent-md
Updated: 2026-05-08T19:12:37.925Z
Author: Stephanie Goodman
Tags: MCP, autonomous agents, Multi-Agent Workflows, AI Powered Infrastructure, Tutorials, Agent Orchestration

---

# How to Run 10+ Playwright Agents on One Logged-In Browser

If you're orchestrating a fleet of agents — a bookkeeping agent pulling statements from various banks; a content design agent creating Canva documents; an AI system where worker agents all need to act as a logged-in user through the browser — you hit the same wall I did. Every agent needs an authenticated session, but giving each one its own browser fails three ways: they all start logged out, they collide if they share a profile directory, and ten parallel sessions launched by automation tooling can trip account safety on stricter sites even when the underlying activity is completely legitimate.

This tutorial walks through the pattern I run today: one real Chromium browser, logged into all your accounts, with any number of Playwright agents attached to it over the Chrome DevTools Protocol. They share the session. They don't collide. The browser looks like your normal browser because it is your normal browser — Playwright is just borrowing tabs.

This is the multi-agent browser automation pattern I reach for whenever an AI agent orchestration system has more than one worker that needs an authenticated browser session. It is the kind of agentic workflow that quietly breaks every other approach to running Playwright in parallel — and once it works, the rest of your AI agent automation stops fighting the browser and starts using it. The same architecture also acts as a clean browser relay for AI browsers, OpenClaw skills, and any other agent runtime that can speak the Chrome DevTools Protocol — more on that in the OpenClaw section below.

**Before you build all this, ask yourself one question.** Do your agents actually need a logged-in identity? If they only need to read public web pages — fetch a doc, scrape a product listing, read a news article — you do not need any of this setup. [AgentPMT's Live Web Page Browser](https://www.agentpmt.com/marketplace/live-web-page-browser) is a marketplace tool that gives agents stateless web access with no persistent login required. Use it when your agents are reading the open web. The AgentPMT web browsing tool provides access to sites the agent's internal browser is blocked from accessing. It returns markdown format, HTML, or screenshots, giving your agent access to exactly what they need. Use the pattern in this tutorial when your agents need to act as you with shared logins.

All the code from this tutorial is in my repo at [github.com/firef1ie/AI-Tutorials/tree/main/playwright-shared](https://github.com/firef1ie/AI-Tutorials/tree/main/playwright-shared).

* * *

## What You'll Build: A Multi-Agent Browser Automation Setup

By the end of this Playwright browser automation tutorial you will have:

-   One Chromium browser running 24/7 on your machine, logged into all your sites
-   A Node.js helper that lets any agent connect to that browser and open its own tab
-   A working example that runs four agents in parallel, all logged in, all in the same shared browser
-   An optional Xvfb setup so the browser never appears on screen
-   A systemd service so it auto-starts at boot and restarts on crash
-   Optional: a Claude Code MCP integration so Claude shares the same logged-in session

Time to complete: about 30 minutes if you're comfortable with Linux and Node.

* * *

## When You Need Multi-Agent Browser Automation

Running multiple AI agents that all act as the same logged-in user is harder than single-agent browser automation makes it look. The cases where you actually need this kind of multi-agent setup:

-   **Content automation pipelines.** Five AI agents posting to your team's social accounts simultaneously, all logged in as the same team identity.
-   **Authenticated web scraping.** Ten workers pulling data from a dashboard you pay for. Per-session rate limits make sharing one login necessary, and per-agent browser automation explodes your resource use.
-   **QA / E2E test automation.** Multiple test workers hitting staging as the same authenticated user, in parallel.
-   **AI agent orchestration.** A research agent that spawns sub-agents, all of which need authenticated browser access to fetch data — agentic workflows that hit a wall the moment session sharing is required.
-   **Personal automation.** One social account, but a draft-reply script, a notifications monitor, and a scheduler all running concurrently.

The common requirement across every case: many parallel agents, one shared browser session, no collisions, no surprise lockouts on accounts that are doing nothing wrong.

* * *

## Why the Obvious Approaches Fail

### Each agent launches its own browser

Every agent starts with a blank profile — logged out. You would have to script the login in each one, and 2FA or captcha breaks that immediately. Ten simultaneous logins from one IP also looks enough like a credential-stuffing attempt that some sites will challenge or lock the account.

### Shared `--user-data-dir`

Pointing every agent at the same profile directory sounds like it would share cookies. It does not work. Chromium places a `ProcessSingleton` lock on the user data directory while it is open. Only one browser process can hold that lock at a time. The second agent errors out or hangs indefinitely.

### Playwright's `storageState` (cookies as JSON)

This is the path most teams try first. Export your login cookies to a JSON file and load that file in every agent's new context. It works fine on most sites — internal tools, SaaS dashboards, staging environments, and the friendlier end of the public-internet spectrum.

Where it gets inconsistent is on stricter sites. Even with valid cookies loaded, Playwright still launches its own Chromium build, and that launch path adds automation defaults Playwright considers part of its testing contract — a `navigator.webdriver` value, an `--enable-automation` flag, and a slightly different Chromium binary than the one installed on your system. Most sites do not care. Some sites are stricter and treat cookie-only sessions launched with those defaults as second-class. Sessions get challenged or invalidated even when the underlying activity is completely legitimate.

If your activity is on the friendlier end of that spectrum, `storageState` is the simpler path. The next section is about the case where you want one shared, persistent, real-user-style browser instead.

* * *

## The Solution: One Browser, Many Agents, via CDP + Xvfb

The fix is to stop letting Playwright launch the browser at all.

Launch one real Chromium yourself via a shell script, the same way a normal user would. Log into your sites once. Leave the browser running. Then have every Playwright agent connect to that already-running browser over the Chrome DevTools Protocol (CDP) — the same debugging interface your browser exposes when you open DevTools.

```
One real Chromium (launched by you, not Playwright)
  -- persistent profile: cookies, history, extensions all live here
  -- remote-debugging-port=9222 exposed on localhost only

        |    CDP over localhost:9222
        |
   Agent 1 → opens a tab, does its work, closes the tab
   Agent 2 → opens a tab, does its work, closes the tab
   Agent N → opens a tab, does its work, closes the tab
```

Because Playwright never launched the browser, none of its launch defaults apply. The browser is whatever you launched it as — a normal user-mode Chromium with a persistent profile and real cookies set by real logins. Playwright is just a CDP client opening tabs and driving navigation inside an existing browser. From any site's perspective, the browser surface is the same surface a human user of that browser would present.

Xvfb (X Virtual Framebuffer) is an optional layer that runs the browser on a virtual display so the window never appears on your screen. Unlike `--headless`, Xvfb runs full rendering on a real X server with no physical monitor attached. The browser is fully non-headless; it just does not have a monitor.

> **Security note.** The CDP endpoint on port 9222 should only ever be bound to `localhost`. Never expose it to a public network. Anyone who can reach that port can drive your browser as you.

* * *

## Prerequisites

-   **Linux** — Ubuntu is used here; any modern distro works
-   **Node.js 20+**
-   **Chromium** — snap or apt install
-   **Xvfb** — optional, only needed for hiding the browser window
-   Basic comfort with a terminal

macOS and Windows work with the same architecture. The install commands and Xvfb section are Linux-specific. macOS can push the window off-screen with `--window-position=-2000,-2000`; Windows users can run inside WSL2.

* * *

## Step 1 — Install Everything

**Node.js.** Use nvm or your package manager. Verify:

```bash
node --version    # v20 or higher
```

**Chromium on Ubuntu via snap:**

```bash
sudo snap install chromium
ls /snap/chromium/current/usr/lib/chromium-browser/chrome
```

Or via apt:

```bash
sudo apt install chromium-browser
```

Note the binary path — you will need it in the launcher script.

**Xvfb (optional, for hiding the browser window):**

```bash
sudo apt install xvfb
```

Playwright gets installed inside the project in the next step. No global install needed.

* * *

## Step 2 — Project Structure

Create the project directory and install dependencies:

```bash
mkdir -p ~/playwright-shared
cd ~/playwright-shared
npm init -y
npm install playwright
npm install --save-dev typescript tsx @types/node
```

The final file layout:

```
~/playwright-shared/
├── package.json
├── tsconfig.json
├── start-browser.sh        ← launches the shared Chromium
├── src/
│   └── connect.ts          ← the helper your agents import
└── examples/
    └── parallel-agents.ts  ← the parallel example
```

Edit `package.json` to set ESM mode and add startup scripts:

```json
{
  "name": "playwright-shared",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start-browser": "./start-browser.sh",
    "example": "tsx examples/parallel-agents.ts"
  },
  "dependencies": { "playwright": "^1.50.0" },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.19.0",
    "typescript": "^5.7.0"
  }
}
```

Create `tsconfig.json`:

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "lib": ["ES2022", "DOM"]
  },
  "include": ["src/**/*", "examples/**/*"]
}
```

* * *

## Step 3 — Write the Launcher Script

This is the shell script that starts your shared Chromium. Save it as `start-browser.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail

CDP_PORT="${CDP_PORT:-9222}"
PROFILE="${PROFILE:-$HOME/.playwright-shared-profile}"
CHROMIUM="${CHROMIUM:-/snap/chromium/current/usr/lib/chromium-browser/chrome}"

if [ ! -x "$CHROMIUM" ]; then
  echo "Chromium not found at $CHROMIUM"
  echo "Override with: CHROMIUM=/path/to/chrome $0"
  exit 1
fi

mkdir -p "$PROFILE"

echo "Launching Chromium on CDP port $CDP_PORT"
echo "Log into your sites in the browser window that opens. Leave it open."

exec "$CHROMIUM" \
  --remote-debugging-port="$CDP_PORT" \
  --user-data-dir="$PROFILE" \
  --no-first-run \
  --no-default-browser-check
```

Make it executable:

```bash
chmod +x start-browser.sh
```

**What each flag does:**

-   `--remote-debugging-port=9222` — opens the CDP endpoint that agents will connect to. By default Chromium binds this to `localhost` only.
-   `--user-data-dir="$PROFILE"` — uses a dedicated profile at `~/.playwright-shared-profile`; cookies, history, and extensions persist here across restarts
-   `--no-first-run` and `--no-default-browser-check` — skip setup dialogs

Notice what is **not** in the script: `--headless` or `--enable-automation`. The launcher behaves like a normal user opening Chromium — no automation-mode defaults are applied.

* * *

## Step 4 — Write the Agent Helper

Save this as `src/connect.ts`. Every agent imports this one function to get a fresh tab in the shared browser:

```ts
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright'

const DEFAULT_CDP_URL = 'http://localhost:9222'

export type ConnectOptions = {
  cdpUrl?: string
  /**
   * 'shared' (default): join the browser's default context — the one with your logins.
   * 'isolated': create a fresh ephemeral context (loses login, gains tab isolation).
   */
  contextMode?: 'shared' | 'isolated'
}

export type Handle = {
  browser: Browser
  context: BrowserContext
  page: Page
  close: () => Promise<void>
}

export async function connectToSharedBrowser(opts: ConnectOptions = {}): Promise<Handle> {
  const cdpUrl = opts.cdpUrl ?? DEFAULT_CDP_URL
  const contextMode = opts.contextMode ?? 'shared'

  let browser: Browser
  try {
    browser = await chromium.connectOverCDP(cdpUrl)
  } catch (err) {
    throw new Error(
      `Could not connect to Chromium at ${cdpUrl}. ` +
      `Did you run npm run start-browser? ` +
      `Error: ${err instanceof Error ? err.message : String(err)}`
    )
  }

  let context: BrowserContext
  let isolated = false
  if (contextMode === 'isolated') {
    context = await browser.newContext()
    isolated = true
  } else {
    const contexts = browser.contexts()
    if (contexts.length === 0) throw new Error(`No default context found at ${cdpUrl}.`)
    context = contexts[0]
  }

  const page = await context.newPage()

  return {
    browser,
    context,
    page,
    close: async () => {
      await page.close().catch(() => {})
      if (isolated) await context.close().catch(() => {})
      // Never close the browser here — other agents are using it.
    },
  }
}
```

The critical line is `chromium.connectOverCDP()` rather than `chromium.launch()`. That single distinction is what keeps the architecture clean. The default `contextMode: 'shared'` uses the browser's existing default context — the one that has your cookies from when you logged in. Each agent opens a new tab via `context.newPage()` and gets its own independent navigation. `close()` shuts down only that tab; the browser keeps running.

* * *

## Step 5 — First Run: Log Into Your Sites

Open a terminal and start the browser:

```bash
cd ~/playwright-shared
npm run start-browser
```

A real Chromium window opens. Log into every site your agents will need: your team's social accounts, internal tools, GitHub, anywhere your agents will operate. Handle 2FA, captchas, and SSO exactly as you normally would — you are logging in as a real user.

Leave the window open. Your cookies are saved in `~/.playwright-shared-profile/`. If you restart the browser later using the same script, those sessions remain (unless a site has expired them).

Verify the CDP endpoint is up:

```bash
curl http://localhost:9222/json/version
```

You should get back a JSON object with a `Browser` field and a `webSocketDebuggerUrl`. That means the browser is reachable by agents.

* * *

## Step 6 — Your First Agent

With the browser still running in the first terminal, open a second terminal and create `examples/single-agent.ts`. Replace the URL with a site you've actually logged into:

```ts
import { connectToSharedBrowser } from '../src/connect.ts'

const { page, close } = await connectToSharedBrowser()

try {
  await page.goto('https://your-site.example.com/home', { waitUntil: 'domcontentloaded' })
  await page.waitForTimeout(2000)

  const url = page.url()
  const title = await page.title()
  console.log('URL:', url)
  console.log('Title:', title)

  if (url.includes('/login')) {
    console.error('NOT LOGGED IN — log into the site in the shared browser first')
  } else {
    console.log('Logged in. Session is shared.')
  }
} finally {
  await close()
}
```

```bash
npx tsx examples/single-agent.ts
```

A tab briefly appears in the shared browser window, navigates, then closes. The terminal should print `Logged in. Session is shared.`

* * *

## Step 7 — Run Many Agents in Parallel

Save as `examples/parallel-agents.ts`:

```ts
import { connectToSharedBrowser } from '../src/connect.ts'

async function runOneAgent(agentIndex: number): Promise<void> {
  const { page, close } = await connectToSharedBrowser()
  try {
    console.log(`[agent ${agentIndex}] navigating`)
    await page.goto('https://your-site.example.com/home', { waitUntil: 'domcontentloaded' })
    await page.waitForTimeout(3000)

    const title = await page.title()
    console.log(`[agent ${agentIndex}] title: ${title}`)

    // Each agent can navigate to different pages, scrape different things,
    // or post different content — all from the same logged-in session.
  } finally {
    await close()
  }
}

// Run 4 agents in parallel
await Promise.all([0, 1, 2, 3].map(runOneAgent))
console.log('all agents done')
```

```bash
npx tsx examples/parallel-agents.ts
```

Four tabs open at once in the shared browser, all navigate, all see the same logged-in session. The terminal output interleaves their progress and ends with `all agents done`. That is multi-agent browser automation working as intended: many AI agents, one identity, no collisions, one shared authenticated session.

* * *

## Step 8 — Hide the Browser With Xvfb

A browser window appearing and stealing focus on a production server is a problem. Xvfb solves this by running the browser on a virtual display — a real X server rendering real pixels to no physical screen.

This is not the same as `--headless`. Headless mode disables real rendering and changes a number of browser internals. Xvfb provides full rendering on a real X server. The browser is fully non-headless; it just does not have a monitor.

> **This step replaces the `start-browser.sh` you wrote in Step 3.** Overwrite it with the version below.

```bash
#!/usr/bin/env bash
set -euo pipefail

CDP_PORT="${CDP_PORT:-9222}"
PROFILE="${PROFILE:-$HOME/.playwright-shared-profile}"
CHROMIUM="${CHROMIUM:-/snap/chromium/current/usr/lib/chromium-browser/chrome}"
USE_XVFB="${USE_XVFB:-1}"
XVFB_DISPLAY="${XVFB_DISPLAY:-:99}"
XVFB_GEOMETRY="${XVFB_GEOMETRY:-1920x1080x24}"

if [ ! -x "$CHROMIUM" ]; then echo "Chromium not found at $CHROMIUM"; exit 1; fi

mkdir -p "$PROFILE"

if [ "$USE_XVFB" = "1" ]; then
  if ! command -v Xvfb >/dev/null; then
    echo "Xvfb not installed. Run: sudo apt install xvfb"
    echo "Or set USE_XVFB=0 to use your real display."
    exit 1
  fi
  if ! xdpyinfo -display "$XVFB_DISPLAY" >/dev/null 2>&1; then
    echo "Starting Xvfb on $XVFB_DISPLAY ($XVFB_GEOMETRY)"
    Xvfb "$XVFB_DISPLAY" -screen 0 "$XVFB_GEOMETRY" -ac &
    XVFB_PID=$!
    for i in {1..50}; do
      xdpyinfo -display "$XVFB_DISPLAY" >/dev/null 2>&1 && break
      sleep 0.1
    done
    trap 'kill $XVFB_PID 2>/dev/null || true' EXIT
  else
    echo "Xvfb already running on $XVFB_DISPLAY, reusing"
  fi
  export DISPLAY="$XVFB_DISPLAY"
fi

echo "Launching Chromium (display: ${DISPLAY:-(real)})"
exec "$CHROMIUM" \
  --remote-debugging-port="$CDP_PORT" \
  --user-data-dir="$PROFILE" \
  --no-first-run \
  --no-default-browser-check
```

**Workflow for first-time login with Xvfb:**

```bash
# Log in once on your real display:
USE_XVFB=0 npm run start-browser
# (a visible window opens — log into your sites, then ctrl+c)

# From then on, run hidden:
npm run start-browser
# (no window appears, but Chromium is running and fully logged in)
```

If you ever need to peek at what the hidden browser is doing, install `x11vnc`:

```bash
sudo apt install x11vnc
x11vnc -display :99 -forever -nopw &
# Connect with any VNC client to localhost:5900
```

**Platform notes:**

-   macOS: use `--window-position=-2000,-2000` to push the window off-screen
-   Windows: run inside WSL2 with Xvfb, or push the window off-screen
-   Headless servers with no display: `xvfb-run npm run start-browser` and you're done

* * *

## Step 9 — Run It as a Service (Auto-Start at Boot)

For production, the browser needs to start at boot and restart automatically if it crashes. A systemd user service handles both without requiring root access.

Create the service file at `~/.config/systemd/user/playwright-shared-browser.service`:

```ini
[Unit]
Description=Shared Chromium for Playwright agents
After=graphical-session.target

[Service]
Type=simple
ExecStart=%h/playwright-shared/start-browser.sh
Restart=on-failure
RestartSec=5
Environment=USE_XVFB=1
Environment=XVFB_DISPLAY=:99
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=default.target
```

(`%h` is a systemd specifier that expands to your home directory.)

Enable and start it:

```bash
systemctl --user daemon-reload
systemctl --user enable playwright-shared-browser
systemctl --user start playwright-shared-browser
systemctl --user status playwright-shared-browser
```

View logs:

```bash
journalctl --user -u playwright-shared-browser -f
```

If you prefer to manage the browser from inside an existing Node.js service rather than a separate systemd unit, spawn it with `child_process.spawn()` and wire up a `child.on('exit')` restart loop to handle crashes automatically.

* * *

## Connecting the Claude Code MCP Server (Optional)

If you use Claude Code, you can point the Playwright MCP server at the shared browser so Claude's interactive browser tools use the same logged-in session:

```bash
claude mcp remove playwright -s user 2>/dev/null
claude mcp add -s user playwright -- npx -y @playwright/mcp@latest \
  --browser chrome \
  --cdp-endpoint http://localhost:9222
```

Verify it connected:

```bash
claude mcp list | grep playwright
```

Restart Claude Code for the config to take effect. From that point on, any `browser_navigate` or `browser_click` call opens a tab in the shared browser with the same login — useful when Claude Code is one of the agents in your orchestration.

If you're building your own MCP servers around this stack — exposing the shared browser as a custom tool to a wider fleet — [AgentPMT's MCP Server Generation](https://www.agentpmt.com/services#mcp-server-generation) handles the scaffolding for you. You bring the capability; it produces the wired-up MCP server.

* * *

## Connecting OpenClaw Skills (Browser Relay)

The same shared Chromium acts as a browser relay for OpenClaw skills. Any OpenClaw skill that needs browser automation — clicking through an authenticated dashboard, posting content, scraping behind a login, taking a screenshot of a page only your account can see — can connect to the same CDP endpoint at `localhost:9222` and open a tab in the shared session.

The OpenClaw browser relay pattern is straightforward when you're already running this setup:

1.  The shared Chromium is your single browser endpoint, exposing CDP on `localhost:9222`
2.  OpenClaw skills point their browser-automation calls at that endpoint
3.  Each skill gets its own tab via the default context — same logged-in cookies, same extensions, same browsing history
4.  No separate Chrome extension to install, no per-skill profile to maintain, no second login flow

This makes the shared browser useful for OpenClaw browser control workloads at the same time as your Playwright agents — both worlds end up sharing one identity. For OpenClaw users who only need stateless web access (a quick HTML fetch, a screenshot of a public page, no authenticated session), point at AgentPMT's [Live Web Page Browser](https://www.agentpmt.com/marketplace/live-web-page-browser) skill instead. Use the CDP-attach pattern only when the OpenClaw skill genuinely needs to act as you on a logged-in site.

For agents whose work crosses from browser automation into AI-generated content — drafting posts, writing replies, summarizing scraped pages — pair this stack with [AgentPMT's AI Writing Quality Check](https://www.agentpmt.com/marketplace/ai-writing-quality-check). It catches voice drift and brand-consistency issues in copy that AI agents produce, which matters once those agents start posting on your behalf through this shared browser.

* * *

## Troubleshooting

**"Could not connect to Chromium at localhost:9222"** The browser is not running, or it is on a different port.

```bash
pgrep -af "chromium.*remote-debugging-port=9222"
curl http://localhost:9222/json/version
```

Start it with `npm run start-browser` or `systemctl --user start playwright-shared-browser`.

**Agents land on the login page instead of the feed** Your session expired or was invalidated. Stop the browser, run `USE_XVFB=0 npm run start-browser` to get a visible window, log in again, then restart the service.

**Port 9222 is in use**

```bash
sudo lsof -i :9222
```

Kill the occupying process, or change `CDP_PORT` in your launcher.

**Xvfb "Server already active for display 99"** A previous Xvfb instance is still running, possibly stale.

```bash
pgrep -af Xvfb
rm -f /tmp/.X99-lock
```

Or pick a different display: `XVFB_DISPLAY=:88 npm run start-browser`.

**A site challenges or rate-limits the session** If a site keeps rate-limiting your session, the issue is almost always one of: too many requests per second from one IP, an IP with poor reputation (datacenter, VPN, Tor exit), a brand-new account with no history, or hitting per-account rate limits on common endpoints. Use a residential connection with an established account, stagger your work, and follow the rate-limit hygiene below.

**Tabs accumulate and are not cleaned up** Force-close the page explicitly:

```ts
await page.close({ runBeforeUnload: false })
```

**High memory usage** Each Chromium tab is roughly 50–200 MB. Close tabs promptly after each agent finishes. Cap parallelism if you are running dozens of concurrent agents.

* * *

## How CDP-Attach Works

For the curious. Skip if you just want it working.

When Playwright calls `chromium.launch()`, it uses an internal launch helper that adds a list of automation defaults to the Chromium command line — `--enable-automation`, certain Blink-feature flags, and a small init script that sets `navigator.webdriver`. These exist because Playwright was built primarily for testing, and tests benefit from a browser that announces itself as automated.

When Playwright calls `connectOverCDP()` instead, none of those defaults apply, because Playwright did not launch the browser. The browser is whatever you launched it as. Playwright is just a CDP client — it opens tabs, navigates them, evaluates scripts, and reads back results. The Chrome DevTools Protocol itself is the same protocol that browser extensions and the DevTools panel use; the browser does not expose any "I'm being controlled" flag to the page based on whether CDP is attached.

So a browser launched the way the launcher script in this tutorial launches it — a normal user-mode Chromium with `--remote-debugging-port` — has the same browser-side surface as the same Chromium opened by double-clicking it from your applications menu. The CDP attachment doesn't change that.

What the attach pattern does **not** change:

-   IP reputation (you're still on whatever IP you have; use a residential ISP)
-   Account history (a brand new account with no posts gets limited regardless)
-   Per-IP rate limits (many requests from one IP look like one user being too active)
-   Behavioral patterns (an agent that clicks instantly with no delays still looks scripted)

The hygiene section below is about staying on the right side of those signals.

* * *

## When You Don't Need This Pattern

CDP attach is the heaviest path. Use it only when you actually need a shared, persistent, logged-in browser. Three lighter options for the cases where you don't:

**Public web access with no login.** Your agents only need to read pages — fetch a public doc, read a product page, pull a news article. There's no account involved. Use [AgentPMT's Live Web Page Browser](https://www.agentpmt.com/marketplace/live-web-page-browser) — a marketplace tool that gives agents stateless web access without any of the setup in this tutorial.

**Friendly sites where automation defaults are fine.** For internal tools, SaaS dashboards, admin panels, and most staging environments, Playwright's `storageState` is much simpler:

```ts
// Bootstrap: log in once, save cookies
const browser = await chromium.launch({ headless: false })
const ctx = await browser.newContext()
// ... log in manually in the opened window ...
await ctx.storageState({ path: 'state.json' })

// Each agent: load the saved cookies
const browser = await chromium.launch()
const ctx = await browser.newContext({ storageState: 'state.json' })
```

No long-running browser process, no Xvfb, no service. Each agent gets full isolation with its own browser. Use this on the friendly end of the spectrum and reach for the CDP pattern only when you genuinely need one shared, persistent, real-user-style browser.

**One agent, not many.** If you're only running a single agent against a logged-in site, you don't need shared-session orchestration at all. Launch a persistent context with `chromium.launchPersistentContext()` and call it a day.

* * *

## Multi-Agent Hygiene at Scale

Even on a clean setup, rate limits still apply at the IP level. A few patterns that help.

**Stagger launches with jitter** — do not throw 50 agents at `Promise.all` at once:

```ts
async function runAgentsStaggered<T>(
  agents: T[],
  worker: (item: T, index: number) => Promise<void>,
  options: { maxConcurrent?: number; minDelayMs?: number; maxDelayMs?: number } = {},
): Promise<void> {
  const { maxConcurrent = 4, minDelayMs = 1000, maxDelayMs = 5000 } = options
  let active = 0; let idx = 0
  await new Promise<void>((resolve) => {
    const tryNext = () => {
      while (active < maxConcurrent && idx < agents.length) {
        const i = idx++; active++
        const delay = minDelayMs + Math.random() * (maxDelayMs - minDelayMs)
        setTimeout(() => {
          worker(agents[i], i)
            .catch((err) => console.error(`agent ${i} failed:`, err))
            .finally(() => {
              active--
              if (active === 0 && idx >= agents.length) resolve()
              else tryNext()
            })
        }, delay)
      }
    }
    tryNext()
  })
}
```

**Add realistic delays inside each agent:**

```ts
import { setTimeout as sleep } from 'node:timers/promises'

function randomDelay(min = 500, max = 2000) {
  return sleep(min + Math.random() * (max - min))
}

await page.click('button.submit')
await randomDelay()
await page.fill('input.search', 'query')
```

**Cache shared reads.** If 10 agents all need the same feed data, fetch it once and share the result. One request, not ten.

**Keep one tab per agent session.** Open the tab at the start of an agent's work session and close it at the end. Opening and closing tabs per individual action is slower, wastes resources, and is less consistent with how a real user actually browses.

* * *

## What You End Up With

Six files plus a profile directory at `~/.playwright-shared-profile/`. No databases, no external infrastructure, nothing beyond what ships on any standard Linux box. The browser carries real history, real cookies, and real extensions from actual use. Only the navigation is automated.

That is the whole shared-session multi-agent browser automation pattern. Many AI agents, one identity, no collisions, one persistent logged-in session. It is the cleanest way I have found to scale Playwright automation past a single worker without reinventing session management or paying the resource cost of one browser per agent. The same setup doubles as a browser relay for OpenClaw skills and as the substrate for AI browsers that need shared logins — Playwright is just the most familiar way in. Full source is at [github.com/firef1ie/AI-Tutorials/tree/main/playwright-shared](https://github.com/firef1ie/AI-Tutorials/tree/main/playwright-shared). For more guides on building agent infrastructure, [view more tutorials](https://www.agentpmt.com/articles?tag=tutorials).