Playwright login-state guide
Connect Playwright to a profile that already remembers login.
storageState files go stale the day a session rotates. A persistent profile is live: connect Playwright over CDP and the login is already there.
Direct answer
Keep the login in an AliasMode profile, start it through the Local API, and connect with chromium.connectOverCDP — no storageState export, no login flows in your tests.
What you will accomplish
Before you start
- AliasMode installed on Windows with the Local API on 127.0.0.1:50400
- A Node.js project with playwright installed
- An AliasMode profile with the target site's login seeded manually
- The profile ID from GET /api/v1/user/list
Step-by-step
Seed the login once, by hand
Open the profile in AliasMode, log into the site, and close the profile. The session now lives in the profile's user-data directory — the durable copy.
Get the profile ID
List profiles and record the ID of the profile you just seeded.
curl 'http://127.0.0.1:50400/api/v1/user/list'Start the profile from the script
Start the browser and capture the CDP websocket endpoint AliasMode returns.
const origin = 'http://127.0.0.1:50400'; const res = await fetch(`${origin}/api/v1/browser/start?user_id=${profileId}`); const payload = await res.json(); if (payload.code !== 0) throw new Error(payload.msg); const cdp = payload.data.ws.puppeteer;Connect over CDP — do not launch
Connect to the running browser instead of launching your own. The profile's cookies, storage, and fingerprint are already live.
import { chromium } from 'playwright'; const browser = await chromium.connectOverCDP(cdp); const context = browser.contexts()[0]; const page = context.pages()[0] ?? await context.newPage(); await page.goto('https://example.com/dashboard'); // already logged inAssert the session, not the login
Treat the logged-in state as an assertion, not a setup step. If the session died, fail loudly and let a human re-seed it.
const loggedIn = await page.locator('[data-testid="account-menu"]').isVisible(); if (!loggedIn) throw new Error('Session expired — re-seed the AliasMode profile');Stop the profile in finally
Always release the session so the profile stays consistent for the next run.
try { // ... test or scrape work ... } finally { await browser.close(); await fetch(`${origin}/api/v1/browser/stop?user_id=${profileId}`); }Compare with storageState honestly
storageState is fine for short-lived fixtures. When a site rotates sessions weekly, the profile wins: the cookie jar stays warm without a refresh pipeline.
CI caveat
In CI, keep AliasMode and the profiles on a long-lived Windows runner or self-hosted agent; ephemeral containers cannot hold persistent sessions.
The AliasMode workflow
Profiles replace storageState
The profile's user-data directory is the durable session store; your repo holds no credentials and no cookie files.
Local API as the lifecycle hook
start and stop bracket every run; the API contract is AdsPower-shaped, so existing tooling maps over.
Fingerprint stability
The deterministic seed means the session always presents the same device, week after week.
Group by project
Keep test identities in their own group so CI and local runs share one source of truth.
Verify it worked
- The first page.goto lands on the logged-in dashboard with zero login code in the script.
- Killing the script mid-run still stops the profile via the finally block.
- Consecutive runs reuse the same session without new cookies warnings.
- GET /api/v1/browser/stop returns success and no chromium process lingers.
Cautions
- Only automate sites in ways their terms allow; a logged-in scripted session is still automation.
- Never print cookies or tokens from the CDP connection into logs.
- Two runs connecting to one profile at once will interleave state — serialize per profile.
The login-state run loop
Resolve
Profile ID from GET /api/v1/user/list, or an env var in CI.
Start
GET /api/v1/browser/start?user_id=... returns data.ws.puppeteer.
Connect
chromium.connectOverCDP(cdp) — the session is already live.
Release
browser.close(), then GET /api/v1/browser/stop?user_id=... in finally.
Login-state strategies compared
| Strategy | Durability | Best for |
|---|---|---|
| Login flow in test | None — re-runs the risk every time | Testing the login itself |
| storageState JSON | Expires with the session | Short-lived fixtures |
| AliasMode profile over CDP | Persistent, self-healing sessions | Long-running scrapers and monitors |
Playwright FAQ
Do I need Puppeteer instead for CDP?
No. Playwright's chromium.connectOverCDP supports CDP endpoints directly; contexts() and pages() work as usual.
What if the site rotates cookies mid-run?
Fine — the profile's cookie jar updates live in the user-data directory, so the next run inherits the rotation.
Can I run several tests against one profile in parallel?
Use separate browser contexts per test if they must be isolated, but serialize the profile itself: one connection at a time.
Sources and verification
- Playwright · Browser.connectOverCDP API reference (checked September 2026)
- Playwright · Authentication and storageState guide (checked September 2026)
- Chrome DevTools Protocol · DevTools Protocol documentation (checked September 2026)
Public product details can change after the check date. Facts are re-checked on a monthly cycle.