Appearance
Persistent Browser Sessions
Keep a browser alive across multiple script invocations so an agent can act, observe, think, then act again — without losing page state.
The problem
The Playwright CLI and one-off scripts launch a fresh browser each time. Page state — navigation history, scroll position, in-memory JS state, form input — is lost between invocations. For iterative workflows where the agent needs to see results before deciding what to do next, this doesn't work.
The pattern
Launch Chromium once with Chrome DevTools Protocol (CDP) remote debugging enabled. Connect to the running browser from separate scripts. Each script connects, acts, and disconnects — but the browser and its pages stay alive.
Start the browser
This pattern requires Chromium. CDP (Chrome DevTools Protocol) is a Chromium feature — WebKit and Firefox don't support it, and Playwright's own launchServer/connect doesn't preserve pages across client disconnections.
Find Playwright's Chromium and launch it with remote debugging:
sh
CHROMIUM=$(node -e "const { chromium } = require('playwright'); console.log(chromium.executablePath())")
"$CHROMIUM" --remote-debugging-port=9222 --no-first-run --no-default-browser-check about:blank &The browser is now running and listening for CDP connections on port 9222.
Connect, act, disconnect
Each agent turn connects to the running browser, does its work, and disconnects. The browser stays alive.
javascript
import { chromium } from "playwright";
const browser = await chromium.connectOverCDP("http://localhost:9222");
const context = browser.contexts()[0];
const page = context.pages()[0];
// Do work
await page.goto("https://example.com");
await page.screenshot({ path: "step1.png" });
// Disconnect — browser stays alive
await browser.close();browser.close() after connectOverCDP() disconnects the CDP client. It does not kill the browser process.
Reconnect on the next turn
The agent sees the screenshot, thinks, then connects again. The page is exactly where it was left:
javascript
import { chromium } from "playwright";
const browser = await chromium.connectOverCDP("http://localhost:9222");
const context = browser.contexts()[0];
const page = context.pages()[0];
// Page state is preserved — still on example.com
console.log(page.url()); // https://example.com/
await page.click("text=More information");
await page.screenshot({ path: "step2.png" });
await browser.close();Stop the browser
When done, kill the Chromium process:
sh
pkill -f "remote-debugging-port=9222"Convenience scripts
Wrap the start/stop/connect lifecycle for easy agent use.
Start
sh
#!/bin/bash
# start-browser.sh — launch persistent Chromium
CHROMIUM=$(node -e "const { chromium } = require('playwright'); console.log(chromium.executablePath())")
"$CHROMIUM" \
--remote-debugging-port=9222 \
--no-first-run \
--no-default-browser-check \
about:blank &
echo "Browser running on CDP port 9222 (PID $!)"Connect and run
A reusable module for connecting to the running browser:
javascript
// connect-browser.js
import { chromium } from "playwright";
export async function connectBrowser(port = 9222) {
const browser = await chromium.connectOverCDP(
`http://localhost:${port}`,
);
const context = browser.contexts()[0];
const page = context.pages()[0];
return { browser, context, page };
}Agent scripts become minimal:
javascript
import { connectBrowser } from "./connect-browser.js";
const { browser, page } = await connectBrowser();
await page.goto("https://example.com/settings");
await page.fill('input[name="email"]', "user@example.com");
await page.click("text=Save");
await page.screenshot({ path: "saved.png" });
await browser.close();When to use what
| Approach | State persists | Best for |
|---|---|---|
CLI (playwright screenshot) | No | One-shot screenshots, PDFs |
| One-off script | No | Scripted multi-step sequences |
| Persistent browser (CDP) | Yes | Iterative agent workflows |
--save-storage / --load-storage | Cookies only | Preserving login across runs |
Use a persistent browser when the agent needs to observe results between steps before deciding what to do next.