To run Playwright in a real browser profile, do not let Playwright launch a browser. Start the profile first, take the debugging address it reports, and attach with chromium.connectOverCDP(). Then use the browser's existing context, which is the profile itself, instead of creating a new one. Your page code stays the same; it simply runs in a browser that keeps its cookies, fingerprint and proxy between runs.
Below: why a freshly launched automation browser looks new every time, what CDP is, the attach pattern in Node and Python, how to keep a signed-in session between runs, and the same idea for Puppeteer and Selenium.
Why a freshly launched browser looks new every time
chromium.launch() is perfect for tests that should start from nothing. For work that depends on who you are to a site, it has three habits:
- No memory. Each launch starts with empty cookies and storage, so every run is a first visit and starts signed out.
- The same machine every time. Every launched browser on one computer presents the same fingerprint: the same screen, fonts, graphics and settings. Run ten scripts in parallel and a site sees ten identical visitors.
- Automation defaults. A launched browser starts with automation switches and default viewports that an ordinary person's browser does not have. How websites recognise automated browsers explains why those details matter.
launchPersistentContext() fixes the first point by keeping cookies in a folder. It does not change the second or third: every folder still looks like the same browser on the same machine.
CDP in one paragraph
The Chrome DevTools Protocol (CDP) is the interface that the browser's developer tools use to inspect and control it. A browser started with remote debugging listens for CDP on a local port. Playwright's connectOverCDP, Puppeteer's connect and ChromeDriver's debuggerAddress all attach through it to a browser that is already running, rather than starting their own.
That is the whole trick: something else starts the browser with the right profile, and your library attaches to it.
Step by step in Node
These steps use Browz, where each profile is a complete, separate browser with its own cookies, storage, fingerprint and proxy.
1. Install Browz and the packages
Install Browz from the download page and open it. Then:
npm install playwright-core @browz/sdkplaywright-core is enough, because Browz provides the browser; there is no npx playwright install. The SDK reads the app's own token from its data folder, so there is nothing to paste.
2. Start a profile and attach
import { chromium } from 'playwright-core';
import { Browz, localCredentials } from '@browz/sdk';
const browz = new Browz(await localCredentials());
const { id } = await browz.profiles.create({ name: 'checkout-test' });
const session = await browz.browser.start(id);
const browser = await chromium.connectOverCDP(session.wsEndpoint ?? session.cdpEndpoint);
try {
const context = browser.contexts()[0]; // the profile's cookies and storage
const page = context.pages()[0] ?? (await context.newPage());
await page.goto('https://example.com');
console.log(await page.title());
} finally {
await browser.close(); // detaches; the browser keeps running
await browz.browser.stop(id); // Browz saves and closes the profile
}3. Mind the two rules
- Attach, never launch.
chromium.launch()would start a different browser with none of the profile's state. - Use the existing context.
browser.contexts()[0]is the profile.browser.newContext()would open an empty, incognito-like context inside it.
And stop the profile through Browz, not by killing the process, so it saves cleanly. On a CDP connection, browser.close() only disconnects.
Step by step in Python
pip install playwright browz-mcpThe Browz Python SDK is in the browz-mcp package and uses only the standard library.
from playwright.sync_api import sync_playwright
from browz.agents import browz_session
with browz_session(name="research-nyc", keep=True) as session, sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session.ws_endpoint or session.cdp_endpoint)
context = browser.contexts[0] # the profile's own cookies and storage
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://example.com")
print(page.title())
browser.close() # detaches; browz_session stops the profilebrowz_session() creates and starts a profile and, on exit, stops it. Without keep=True it also deletes it, which suits one-off jobs.
Keep a signed-in session between runs
The point of a real profile is that it remembers. A simple pattern for scripts that work on an account you are entitled to use:
- Create the profile once and give it a name, such as
research-nyc, and a proxy if the work needs a particular region. - Open it from the Browz app and sign in yourself in its window. A person completes the sign-in, any 2FA code and any CAPTCHA.
- Close it. The cookies and storage stay in the profile.
- Let your script find the profile by name and attach. With
reuse: true(Node) orreuse=True(Python), Browz returns the endpoints of a profile that is already open instead of refusing, and the script stops it only if it started it.
const profile = (await browz.profiles.list()).find((p) => p.name === 'research-nyc');
const session = await browz.browser.start(profile.id, { reuse: true });
// …attach and work as above…
if (!session.alreadyRunning) await browz.browser.stop(profile.id);For many parallel jobs that should never share state, withBrowzSession() in the Node SDK creates a throwaway profile, runs your function and then stops, deletes and purges it, even when the function throws. Run several side by side and they never share a cookie.
Headless, Playwright Test and the command line
- Headless:
browz.browser.start(id, { headless: true }). In our own fingerprint test on 13 November 2026, Browz 0.1.9 on macOS passed 15 of 15 consistency checks both headed and headless (the raw result). - Playwright Test: start the profile in a fixture or
globalSetup, connect withchromium.connectOverCDP, and stop it in teardown. Use the default context, not Playwright's per-test contexts. - Without code: the
browzcommand prints the endpoint for any library.browz --json start "Shop – Sydney" | jq -r .cdpEndpointstarts a profile by name and gives you its CDP address;browz stop "Shop – Sydney"saves and closes it.
The same idea in Puppeteer and Selenium
- Puppeteer: install
puppeteer-core, start the profile, thenpuppeteer.connect({ browserWSEndpoint: session.wsEndpoint, defaultViewport: null }).defaultViewport: nullkeeps the profile's real window size instead of Puppeteer's 800×600. Finish withbrowser.disconnect(), notclose(). The Puppeteer guide has the full script. - Selenium: ChromeDriver attaches through the
debuggerAddressoption, pointed at the profile's debug port. Use a ChromeDriver whose major version matches the Browz engine, and pass its path yourself. The Selenium guide shows how to read the version and a pytest fixture with one fresh profile per test.
What Browz adds to the attach pattern
Attaching to a profile is only as good as the profile. In Browz, each profile's fingerprint is set in the browser engine itself, not by a script injected into the page, and it is one consistent device derived from a real device type, stable from one session to the next. Driving it with Playwright, Puppeteer or MCP does not change the device or network fingerprint it presents (anti-detect, measured). Its timezone, languages and location follow the proxy's measured exit.
The Local API, CDP and the SDKs are on every plan, Free included, and are not metered. Free covers 5 profiles; paid plans start at $4.99 a month. Complete, tested examples are in the agent recipes, and the Playwright guide and Playwright for Python guide go further. Automate only where each site's terms allow it; our acceptable use policy applies to scripts the same as to people.
Frequently asked questions
What is the difference between connectOverCDP and launch in Playwright?
launch() starts a new browser process that Playwright owns, with a fresh, empty state. connectOverCDP() attaches to a browser that is already running and exposes the Chrome DevTools Protocol. Use it when another tool, such as Browz, starts the browser with a saved profile.
Why does browser.newContext() lose my cookies?
A new context is a separate, empty session inside the browser, much like an incognito window. The profile's cookies and storage live in the default context, which you get with browser.contexts()[0].
Does browser.close() stop the profile?
No. On a browser you attached to over CDP, close() disconnects Playwright and leaves the browser running. Stop the profile through Browz, with browz.browser.stop(id) or browz stop, so it saves its state cleanly.
Which Playwright version do I need?
Any recent version with connectOverCDP. Use the chromium browser type, not firefox or webkit. The Browz recipes pin a reviewed version of playwright-core if you want the exact one we test with.
Can I use Playwright with a profile I also use by hand?
Yes. Start it with reuse: true and the script attaches to the window you already have open, then leaves it running. Just avoid typing in it while the script is working, as both of you would be driving the same page.