BLOG
Profile List for Browser Agents: Persistent Sessions
A profile list maps named browser profiles to hosted Chromium sessions. Learn how to design, store, and reuse profiles for AI agents and automation.

A profile list is the set of named browser profiles your automation can start a session from — each one carrying its own cookies, local storage, saved logins, and browser settings. For AI agents and browser automation, the profile list is the difference between a fresh anonymous browser on every run and a session that already knows who it is. On a hosted runtime like Remote Browser, you define profiles once, reference them by name or ID when creating a session, and get consistent state across runs without shipping a local Chrome user-data directory into production.
This guide covers what belongs in a profile list, how to design it for agents, how to wire it into Playwright over CDP, and the production criteria that separate a workable setup from one that breaks at scale.
What a profile list actually contains
A browser profile is the on-disk state Chrome keeps for a user: cookies, localStorage, IndexedDB, cache, saved passwords, autofill data, extensions, and preference flags. A profile list is simply the inventory of those profiles your runtime can launch.
In a hosted browser runtime, the profile list is usually a control-plane concept. Each entry has:
- An identifier — a stable name or ID your code passes when creating a session.
- Storage backing — where the profile data lives between sessions (object storage, a volume, or a managed store).
- Browser settings — viewport, locale, timezone, user agent overrides, and proxy configuration.
- Lifecycle policy — whether the profile persists after a session ends, how long it is retained, and who can read it.
- Ownership metadata — which tenant, agent, or workflow the profile belongs to.
The important distinction: a profile is *state*, a session is *runtime*. The profile list is the catalog of state; sessions are ephemeral processes that mount a profile, do work, and write state back.
Profile vs. session vs. context
These three get conflated constantly, and the confusion causes real bugs.
| Concept | Lifetime | What it holds | Scope |
|---|---|---|---|
| Profile | Long-lived, persists across sessions | Cookies, storage, logins, settings | Per identity or tenant |
| Session | Minutes to hours | A running Chromium process | One per task or worker |
| Browser context | Tied to a session | Isolated tabs and storage within a session | One or more per session |
A profile list is a list of profiles, not sessions. You can run many sessions against the same profile — though usually you shouldn't run them concurrently, because two processes writing the same cookie jar will clobber each other. If you need parallel work under one identity, either serialize the writes or split the identity across profiles.
Why agents need a profile list at all
A stateless agent re-authenticates on every run. That means re-solving logins, re-accepting cookie banners, re-walking onboarding flows, and re-triggering any risk checks that fire on new devices. Each of those steps is a failure point and a latency cost.
A profile list solves three concrete problems:
- Authentication persistence. The agent starts already logged in because the session cookies are in the profile. No credential handling in the agent loop.
- Consistency. The same profile presents the same browser settings and storage each run, so behavior is reproducible. This matters for debugging — you can replay a failing task against the same profile state.
- Isolation. Different tenants, customers, or tasks get different profiles. One agent's cookies never leak into another's session.
For AI browser agents specifically, persistence also improves task success. Sites that gate content behind a session, a consent state, or a region setting behave predictably when the profile already satisfies those conditions. The agent spends its steps on the actual task instead of on setup.
Designing a profile list for production
The naive approach — one profile per agent — falls apart quickly. Here is a structure that holds up.
Key profiles by identity, not by task
A profile represents *who* the browser is, not *what* it is doing. If you create a profile per task, you get thousands of near-identical profiles with no reuse and no benefit. Key by the identity that matters to the target site: a customer account, a tenant, a region, or a service account.
tenant:acme:region:eu → profile for Acme's EU operations
tenant:acme:region:us → profile for Acme's US operations
service:billing-portal → shared service account profileSeparate read-only and write profiles
Some workflows only read (scraping public pages behind a login). Others write (submitting forms, changing account state). Give write workflows their own profiles so a runaway agent cannot corrupt the state a read workflow depends on.
Plan for profile drift
Profiles accumulate state. Cookies expire, storage grows, and stale sessions cause intermittent failures. Build a policy:
- Rotation — retire and recreate profiles on a schedule for high-churn identities.
- Reset on failure — if a session fails authentication checks, reset the profile rather than retrying blindly.
- Retention limits — delete profiles for closed tenants. Profile data is user data; treat it like it.
Keep secrets out of the profile
A profile can hold a logged-in session, but it should not be your credential store. Keep passwords and API keys in a secrets manager, and use the profile only for the resulting session state. If a profile is compromised, you rotate the session, not the credential.
Wiring a profile list into Playwright over CDP
Hosted Chromium sessions expose a CDP endpoint. You create a session with a profile reference, get back a WebSocket URL, and connect Playwright to it. The profile list lives on the control plane; your code only passes the identifier.
The example below assumes a small client that creates sessions and returns a CDP endpoint. Replace the endpoint shapes with your provider's actual API — the pattern is what matters.
import { chromium, Browser, BrowserContext } from "playwright";
type ProfileRef = {
profileId: string;
// Optional per-session overrides layered on top of the profile
locale?: string;
timezoneId?: string;
};
type SessionHandle = {
sessionId: string;
cdpUrl: string; // wss://... from your runtime's session-create call
};
// Your runtime client. The profile list is resolved server-side.
async function createSession(profile: ProfileRef): Promise<SessionHandle> {
const res = await fetch("https://api.remote-browser.dev/v1/sessions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.REMOTE_BROWSER_API_KEY}`,
},
body: JSON.stringify({
profile_id: profile.profileId,
locale: profile.locale,
timezone_id: profile.timezoneId,
}),
});
if (!res.ok) {
throw new Error(`session create failed: ${res.status} ${await res.text()}`);
}
return (await res.json()) as SessionHandle;
}
async function runTask(profile: ProfileRef) {
const session = await createSession(profile);
let browser: Browser | undefined;
try {
// Connect over CDP to the hosted Chromium instance
browser = await chromium.connectOverCDP(session.cdpUrl);
// Reuse the default context that carries the profile's storage state
const context: BrowserContext = browser.contexts()[0];
const page = await context.newPage();
await page.goto("https://example.com/dashboard", {
waitUntil: "domcontentloaded",
});
// If the profile is valid, we land authenticated.
const loggedIn = await page.locator("[data-testid=account-menu]").isVisible();
if (!loggedIn) {
throw new Error(`profile ${profile.profileId} is not authenticated`);
}
// ... agent work here ...
} finally {
// Closing the connection does not necessarily end the session;
// call your runtime's stop endpoint to release the browser.
await browser?.close();
}
}Two details worth calling out:
- `connectOverCDP` returns a browser with existing contexts. The profile's storage is already mounted in the default context. Don't create a fresh context expecting the profile state — it won't be there.
- Closing the CDP connection is not the same as stopping the session. Use your runtime's session-stop call so the profile is flushed and the browser-hour stops accruing. See the documentation for the exact lifecycle endpoints.
The Playwright reference for this connection method is in the official `connectOverCDP` docs, and the underlying protocol is documented in the Chrome DevTools Protocol.
Profile lists across runtimes
If you are evaluating where to run this, the profile list is one of the sharper differentiators. Here is how the common options compare.
| Runtime type | Profile list support | Persistence model | Main trade-off |
|---|---|---|---|
Local Chrome + launchPersistentContext | Manual — one directory per profile | Files on disk | No isolation across machines; hard to scale |
| Self-hosted Playwright grid | Custom — you build the store | Your object storage | You own rotation, cleanup, and access control |
| Hosted browser runtime (Remote Browser) | Managed — named profiles via API | Provider-managed, per-session mount | Less control over storage internals |
| Browser extension / MCP bridge | Limited — tied to the local browser | Local browser profile | Not suitable for headless scale |
The self-hosted path is viable if you already run infrastructure and want full control over where profile data lives. The hosted path removes the storage and lifecycle work, which is usually the part that breaks first. For a broader comparison of runtime choices, see Remote Browser for AI agents.
Production criteria for a profile list
Before you commit to a design, check these against your workload.
- Concurrency semantics. Can two sessions mount the same profile at once? If yes, how are writes reconciled? Assume last-write-wins unless documented otherwise, and serialize writes to shared profiles.
- Isolation guarantees. Are profiles scoped per tenant at the storage layer, or only by naming convention? Naming conventions fail under pressure.
- Data residency. Profile data is user data. If you serve EU customers, the profile store needs to respect that.
- Retention and deletion. Can you delete a profile and prove it is gone? GDPR-style deletion requests will hit your profile list eventually.
- Observability. Can you see which profile a session used, when it was last written, and whether it is healthy? A live viewer helps here — you can watch a session and confirm the profile loaded the expected state. See remote control browser.
- Cost model. Profile storage and session time are usually billed separately. Check pricing for how each is metered before you size the list.
Common mistakes
Creating a profile per task. This defeats the purpose. Profiles should be reused across tasks that share an identity.
Sharing one profile across all agents. Convenient until one agent's failed login invalidates the session for everyone. Isolate by identity.
Ignoring profile size. Storage grows. A profile that has been running for months may carry megabytes of stale cache and expired tokens. Rotate.
Treating the profile as a backup. A profile is live state, not an archive. If you need an audit trail, record sessions separately.
Assuming the profile is portable. A profile captured in one runtime may not import cleanly into another. Test migration before you depend on it.
Where to go next
If you are moving from local Chrome to a hosted runtime, the profile list is the first thing to design, because everything downstream — authentication, isolation, debugging — depends on it. Start by enumerating the identities your agents act as, map each to a profile, and decide the rotation policy before you write the first session-create call.
For setup details, read the documentation. For a walkthrough of running Chromium without local installs, see remote browser online. And if you are still choosing a runtime, remote web browser covers the trade-offs in more depth.