Persistent profiles
Launch with "profile": "shop-login" and the browser starts from that profile: the cookies, localStorage and IndexedDB of the last session that used it. When the session ends, the profile is saved again. Log in once and stay logged in across sessions, machines and days — with no state files to store yourself.
Use it
{
"proxy": "http://user:[email protected]:8080",
"profile": "shop-login"
}
The first launch with a new name creates the profile; every later launch with the same name restores it. Names are up to 64 letters, digits, - or _. Then work in browser.contexts()[0] — that context is the profile:
import { chromium } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
async function launch() {
const res = await fetch('https://starter.cdpfleet.com/chrome/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: true, profile: 'shop-login' }),
});
if (!res.ok) throw new Error(`launch: ${res.status} ${await res.text()}`);
return chromium.connect((await res.json()).wsUrl, { headers: { 'x-api-key': KEY } });
}
// First run: log in once. The profile is saved when the session ends.
let browser = await launch();
let context = browser.contexts()[0]; // the profile — not browser.newContext()
let page = context.pages()[0] ?? await context.newPage();
await page.goto('https://example.com/login');
// ... log in ...
await browser.close();
// Any later run, from any machine: already logged in.
browser = await launch();
context = browser.contexts()[0];
page = context.pages()[0] ?? await context.newPage();
await page.goto('https://example.com/account');
await browser.close();
# The same in Python (sync API): use browser.contexts[0]
browser = p.chromium.connect(ws_url, headers={"x-api-key": KEY})
context = browser.contexts[0] # the profile — not browser.new_context()
page = context.pages[0] if context.pages else context.new_page()
Use contexts()[0], not newContext(). A context you create with newContext() inside a profile session is a throwaway incognito context: nothing in it is saved. This holds for Playwright connect() and for connectOverCDP (CDP) alike.
What is saved
- Saved: cookies — session cookies (no expiry date) included — localStorage and IndexedDB: where logins, "remember me" tokens and site preferences live. Session cookies are captured just before the browser closes and put back on the next launch, so logins that live in one (
PHPSESSID,ASP.NET_SessionId…) survive as long as the site keeps that session on its server. - Not saved: sessionStorage, which browsers keep per tab — data a page stores there is gone on the next launch, as after a browser restart.
- Not saved: caches (HTTP, code, GPU and service-worker caches) — they make up most of a profile's size and are rebuilt on use. A saved profile is typically a few kilobytes to a few megabytes.
- When: after the session ends and the browser has exited (a clean save, never a half-written database). If a node fails mid-session, that session's changes are lost and the profile keeps its last saved version.
- Camoufox keeps its device: the fingerprint generated on a profile's first launch (user agent, screen, fonts, WebGL…) is saved with it and reused, so a site sees the same device every time. Later launches ignore fingerprint-shaping options (
os,screen_size,webgl_config,fonts,configfingerprint keys). Timezone, locale and geolocation still follow the exit IP — keep a profile on one country or a sticky exit, since a device that moves between countries is itself suspicious. The canvas is the one exception: like stock Firefox, Camoufox's built-in fingerprinting protection changes what a page reads back from a canvas in every browser session, so canvas hashes differ between launches even with the same profile.
Read-only, and one writer at a time
"profile": {"id": "shop-login", "mode": "readonly"}restores the profile and never saves it — run as many read-only sessions in parallel as you like, for example scrapers sharing one login.- A normal (
"rw") session holds the profile until it ends: a secondrwlaunch gets409 profile_in_use. - Works with
keep_alive(saved when the session finally ends), live view — log in by hand once with Take control, then let code reuse the profile — and recordings.
Browsers and limits
- Browsers: Google Chrome, Microsoft Edge, Brave, Opera, Yandex Browser, Chromium, Patchright, Camoufox, Firefox and WebKit. Naver Whale and Rebrowser return
400 profile_unsupported_engine. - A profile belongs to the browser it was created with (
409 profile_engine_mismatchon another) and can't be opened by an older major version than it was last saved with (409 profile_version_downgrade). - Limits: 20 profiles per shared thread and 30 per dedicated thread (at least 100 per account), 50 MB per saved profile (a larger save is skipped and the previous version kept). Profiles are kept until you delete them. Included in every paid plan.
- Over the limit (after moving to fewer threads, to Community, or when a plan ends): nothing is deleted. New profiles can't be created; 30 days later the profiles beyond the limit are frozen — the ones you mark "keep" stay active first, then the most recently used. A frozen profile is listed but can't be launched (
403 profile_frozen); upgrading unfreezes it at once. Profiles frozen for 90 days are deleted. Manage them in the dashboard (Sessions → Persistent profiles) or with the API below.
Manage profiles
GET https://cdpfleet.com/v1/me/profiles # your limit, and each profile: name, browser, size, last used, in use, frozen, keep
PUT https://cdpfleet.com/v1/me/profiles/{name} # {"keep": true} — stays active if the account goes over its limit
DELETE https://cdpfleet.com/v1/me/profiles/{name} # 409 while a session is using it
Errors
| Status | Error | Meaning |
|---|---|---|
| 400 | invalid_profile | Not a valid name or {id, mode} object. |
| 400 | profile_unsupported_engine | Profiles aren't available on this browser yet. |
| 403 | profile_limit | The account already has as many profiles as its plan allows; delete one first, or add threads. |
| 403 | profile_frozen | The profile is frozen because the account is over its plan's limit; upgrade, or delete other profiles. |
| 404 | no_such_profile | A read-only launch of a profile that doesn't exist yet. |
| 409 | profile_in_use | Another session is writing this profile; retry when it ends, or launch read-only. |
| 409 | profile_engine_mismatch | The profile was created with another browser. |
| 409 | profile_version_downgrade | This browser version is older than the one that last saved the profile. |
| 503 | profile_restore_failed / profile_service_unavailable | The profile couldn't be loaded right now; retry. |
Security
Profiles hold logins, so they're stored encrypted with a key of their own (AES-256-GCM) in our private object storage in Germany, and the key is kept separately from the data. Browser servers never hold storage credentials: each session gets one-time links to load and save its profile, decrypts it in memory and wipes it after the save. Only your account can use or list your profiles.
Profiles or storageState?
Playwright's storageState does a similar job from your side: you save cookies and storage to a file and load it into a new context (case 11; IndexedDB needs an option, case 39). Profiles keep the state on our side, keep a Camoufox device stable and need no file handling. Use storageState when you want the state in your own hands, or on browsers profiles don't cover yet; see case 40 for exactly what a profile brings back.