
A persistent browser session is one whose authentication state outlives the process that created it. The session is established once, usually by a human; the cookies, local storage, and profile data are written somewhere durable; and a later run starts already signed in instead of starting at a login form.
Getting that to work the first time is easy. Keeping it working is the hard part, because four different things kill sessions and they all look identical from the outside: the profile directory is not being reused, the site issued a session cookie that never reaches disk, the profile is locked by another browser, or the session simply expired on the site's own schedule. This article walks the full lifecycle so you can tell those apart.
What makes a browser session persistent?
Two things have to be true: the authentication state has to be written somewhere that survives the process, and the next run has to be pointed at that same place. If either half is missing, you get a one-time session.
That distinction matters more for agents than for tests. A test suite that signs in at the start of every run is merely slow. An agent asked to check a dashboard every morning cannot re-authenticate unattended, because the sign-in itself is the part that needs a human. Persistence is what converts a one-shot authenticated action into a repeatable one.
The six stages of a session lifecycle
Most persistence problems get misdiagnosed because the failure is reported as a symptom rather than a stage. Sorting the work into six stages makes the diagnosis mechanical, because each stage has its own failure mode and its own fix.
| Stage | What it decides | Typical failure at this stage |
|---|---|---|
| Establish | Where the session is created and by whom | Automating the sign-in, then tripping on MFA or a bot check |
| Preserve | Which state is written, and where the next run reads it | A fresh temporary profile each run; state written but never loaded |
| Verify | Whether the restored session is still accepted | A restored but expired session; the agent reports an application bug |
| Isolate | Who may use the state at the same time | Two agents on one profile: a lock error, or a silent collision |
| Expire | How the session ends and how you detect it | Session cookies never reached disk; the site revoked the session |
| Recover | How the session gets re-established | No re-auth path exists, so the workflow stalls until someone notices |
The rest of this article takes the stages in order, with the mechanism comparison first, because the stage that fails is usually decided by the mechanism you picked at the start.
userDataDir, storageState, or a managed session?
Four mechanisms show up in practice. They differ on what they persist, whether they can run concurrently, and how much of your real browsing environment they borrow.
| Mechanism | Persists | Durable across | Not for |
|---|---|---|---|
| Persistent user data directory | The whole profile: cookies, local storage, cache, extensions, installed state | Process restarts that reuse the same directory path | Parallel runs, or any task that needs a clean, reproducible environment |
| Exported storage state | Cookies plus per-origin localStorage, optionally IndexedDB and passkeys | Anything that can read the file, including CI and another machine | Sites that bind sessions to device signals the snapshot does not carry |
| Managed remote session | A live browser kept running on remote infrastructure, or a named context saved server-side | Requests that arrive before the provider's idle timeout closes it | Per-user isolation, unless the provider routes specific users to specific instances |
| A task Space in a local agent browser | The login environment the browser already holds, reused inside a task-scoped Space with separate tabs | Runs inside that browser, for as long as the underlying session lasts | Headless CI, or any setup where the browser cannot stay installed and signed in |
What the state file actually contains
In Playwright, browserContext.storageState() returns cookies as an array whose entries carry name, value, domain, path, expires, httpOnly, secure, and sameSite, plus an origins array holding origin and localStorage name/value pairs. Passing a path writes the JSON to disk; without a path the state is returned but not saved. The guide describes reused state as covering cookies, local storage, IndexedDB, and passkey-based authentication.
That file is credential-grade. The official guide warns it "may contain sensitive cookies and headers that could be used to impersonate you or your test account" and strongly discourages checking it into any repository. Treat it exactly like a password: outside the working tree, outside logs, outside the agent's context.
How do you establish the session?
Establish it once, with a human in the loop, in the same environment the agent will later use. This is the stage where automating the sign-in looks tempting and usually costs more than it saves, because MFA prompts, device checks, SSO redirects, and bot checks all interrupt a scripted sign-in, and each workaround pulls credentials closer to the agent's context.
The practical rule is that the human types the credentials and the agent inherits the result. That is also why the environment matters: a session established in a headed browser on your laptop and then restored in a headless container on a server is a session moving between two different device fingerprints, which some sites notice.
If you need to choose where that first sign-in should happen, and which of the connection routes to use, that decision has its own article: How to connect AI agents to your existing browser. It covers DevTools auto-connect, extension injection, CDP takeover, and state copies. This article assumes you picked one and now need it to keep working.
How do you preserve it between runs?
Preservation is a path problem before it is a browser problem. In Playwright, a normal browser context lives in memory and shares nothing with other contexts, so it is gone when the process ends. A persistent context changes that by taking a user data directory as its first argument, described in the API reference as the path that "stores browser session data like cookies and local storage." Cookies and local storage go to that directory and are read back on the next launch.
The alternative is to leave the browser stateless and carry the state in a file: export with storageState(), then seed the next context with it. The official authentication guide builds its whole recommended workflow on this, writing state to a playwright/.auth directory that belongs in .gitignore.
The two are not interchangeable, and the decision comes down to one question: does the target site care that it is the same browser, or only that it is the same account? If the site only checks the cookie, an exported state file is lighter, portable, and safe to run in parallel. If the site correlates device, timezone, language, and cache signals across visits, the full directory is what keeps those signals coherent.
How do you verify the session is still valid?
Nothing in the persistence mechanism checks validity. A restored cookie that the site has since revoked restores perfectly and fails on first use, and the agent then reports whatever the site rendered, which is usually a login page or an empty dashboard. Verification is therefore a step you add, not a feature you enable.
Three checks cover most cases. Navigate to a page that requires authentication and assert on a post-login element you know exists, rather than on the absence of an error. Compare the final URL against the login URL, since a redirect to sign-in is the clearest possible signal. And keep an authenticated marker outside the page itself, such as an account identifier in the response, so a page that renders an empty shell does not read as success.
What that looks like in practice is less dramatic than it sounds. In a live run for this article, a Space held an authenticated store session with one item in the cart, and the same state was still present after a page reload in that Space. The check was not "did the page load" but "is the badge still 1 and does the product row still read Remove": state that only exists if the session is genuinely alive.


The same run provides the contrast case. After the session ended, navigating to the protected inventory route did not render an empty page: it returned a login page carrying an explicit rejection message. That is what a lost session looks like from the page side, and it is far easier to assert on than a blank screen.

Add that verification to the start of every run and the failure moves from "the agent gave a wrong answer" to "the session needs re-establishing", which is a much cheaper problem.
What happens when two agents share one profile?
A persistent profile is single-tenant, and this is documented rather than incidental. The Playwright API reference states plainly that "browsers do not allow launching multiple instances with the same User Data Directory." The Playwright MCP documentation is more specific about the consequence: "A profile can only be used by one browser at a time," and if it is already locked, "the server fails to start." That is the good version of the failure, because it fails loudly at launch instead of silently sharing cookies.
The bad version is the silent one. If two tasks end up pointed at the same state, they can write over each other's cookies and localStorage: one task's logout, or its switch to a different account, becomes the other task's session. This is the mechanism behind a class of bug that looks like flakiness, where two runs pass individually and fail together.
Managed remote sessions have a related but different isolation problem. A session-reuse pattern that picks any free browser off a list, rather than routing a specific user to a specific instance, can hand one request a browser another user just used. The vendor documentation for that pattern describes it as best for "stateless workloads where any available browser session will do," which is an honest description of when the isolation question does not apply.
The three workable answers are: one profile per agent, one account per parallel worker, or one task-scoped workspace with separate tabs. The official authentication guide takes the second route, allocating a separate account per parallel worker with its own state file, and tells you to authenticate in a clean environment by unsetting storage state first. An agent Space provides the third route inside a browser. It separates task tabs and control, but it is not by itself a process, VM, or tenant-security boundary.
| Isolation need | Approach | Cost |
|---|---|---|
| Two tasks, same account, no overlap in time | One shared profile, serialized with a queue or a lock you own | The tasks cannot run in parallel; you must handle lock contention yourself |
| Two tasks running at once, same site | Separate profiles, or one account per worker with its own state file | More accounts or more disk; each profile re-authenticates separately |
| Many tasks, shared login, no shared writes | Per-task Space that reuses explicitly provisioned browser state while keeping task tabs separate | Requires a browser that models per-task workspaces natively; this does not provide remote host isolation |
Why did the session stop working?
Four causes account for most cases, and they are distinguishable by when the failure appears.
| Symptom | Likely cause | What to check |
|---|---|---|
| Signed in within one process, signed out on the next | Session cookies were never written to disk; the profile only holds persistent cookies | Export cookies through the browser API and re-add them after launch, rather than trusting the profile file |
| The browser refuses to start, or the server exits at launch | Another browser instance holds the profile lock, often an orphaned process from a crashed run | Remove or wait out the stale process, and give each concurrent task its own directory |
| Worked for days, then stopped with no code change | The site expired or revoked the session on its own schedule | Nothing in your code. This is the re-establish path, and it needs a human |
| Works locally, fails in CI or on a server | State file missing or not loaded there, or the session is bound to signals the new environment does not reproduce | Confirm the file is present at runtime and that the run actually reads it; then check whether the site correlates device signals |
How do you recover an expired session?
Recovery is not the inverse of persistence, and it cannot be fully automated. The official guidance is direct about the maintenance burden: "you need to delete the stored state when it expires." What follows is the procedure that turns that into something an agent can drive.
- Detect, do not assume. Run the verification check at the start of the task, before any work that depends on being signed in.
- Stop at the boundary. An expired session is a stop condition, not a retry condition. Retrying against a revoked session produces the same failure and, on sites that count attempts, makes things worse.
- Hand the visible window to a human for the sign-in, including whatever second factor the site requires, then resume in the same session.
- Re-export the state after the human finishes, and replace the stored copy rather than appending to it.
- Re-run the verification check to confirm the new state works, so the next unattended run starts from a known-good position.
Step five is the one most setups skip, and it is the one that decides whether the failure recurs on the next run or not. In the run behind this article, the recovery path was exactly this shape: after the session ended, a human signed in again in the same Space, and the authenticated state was re-confirmed afterwards rather than assumed.

When should a human take over?
Persistence changes what a human has to do, not whether one is needed. The moments that require a person are stable and predictable: a first sign-in to establish the session, any second factor, QR or hardware-key logins, consent and permission screens, payment or irreversible actions, and re-authentication after expiry.
Design for those as handoffs instead of exceptions. The practical test is whether the human is asked to act inside the same session the agent will continue in. If the person signs in somewhere else and the agent keeps a different browser, the handoff did not happen; the agent is simply waiting for a state it will never see.
Where ego (lite) fits
ego (lite) is a complete local Chromium browser designed for people and AI agents; in product-category terms, it is an agent browser. You can use it as an everyday browser, while compatible agents control it through ego-browser. During onboarding, ego (lite) can import Chrome tabs, bookmarks, passwords, extensions, cookies, login sessions, and profiles, so an agent can start from browser state you explicitly provision instead of signing in from scratch. That import does not guarantee that every website session remains valid: cookie expiry, re-authentication, 2FA, CAPTCHA, site policy, and account permissions still apply.
Each agent task runs in its own visible Space with separate tabs, while you continue browsing elsewhere. When a site requires sign-in or verification, you can take over that same Space, complete the step yourself, and return control to the agent. A Space makes that handoff practical, but its workspace boundary is not a remote security sandbox and it does not guarantee that a particular session is still valid.
ego (lite) fits local browser work that benefits from user-provisioned login state, visible execution, several parallel Spaces, and quick human takeover. It is not the right tool when the task must run headless in CI, start from a reproducible clean image, scale as an elastic remote fleet, or keep untrusted pages off the local machine. In those cases, use an exported test state with ordinary browser contexts, a remote sandbox, or a hybrid architecture.
Troubleshooting checklist
Work down the list in order. The earlier items are cheaper to check and cause more failures than the later ones.
- Confirm the launch actually reused your state path. A fresh temporary profile each run is the most common cause, and it looks exactly like expiry.
- Verify the session instead of trusting it: assert on a post-login element and on the final URL, not on the absence of an error.
- If it fails across processes but works within one, suspect session cookies that never reached disk, and export cookies through the browser API.
- If the browser will not start, check for a profile lock from a crashed or still-running instance on the same directory.
- If two tasks interfere, they are sharing state. Give each one its own profile or its own account before looking anywhere else.
- If it fails only on a server, confirm the state file is present and loaded at runtime, then consider whether the site correlates device signals.
- If it worked for days and then stopped, it is site-side expiry. Re-establish with a human and re-export the state.
- Re-run the verification after every recovery, so the next unattended run starts from a confirmed session.
FAQ
How long does a persistent browser session last?
As long as the site lets it. Your mechanism sets the ceiling and the site sets the floor: the profile directory will happily hold a cookie forever, while the server can invalidate it in minutes or after a fixed number of days. That is why verification belongs in the run rather than in a one-time setup check.
Is storageState enough, or do I need a full profile?
An exported state file is enough when the site authenticates on cookies and localStorage and does not otherwise care which browser is visiting. A full profile is the safer choice when the site correlates device, timezone, locale, or cache signals across visits, because the directory keeps those consistent while a state file does not.
Can two agents use the same persistent session at once?
Not against the same user data directory. Official documentation states that browsers do not allow multiple instances on one user data directory, and that a locked profile stops the server from starting. If two tasks genuinely need to run concurrently, give them separate profiles or separate accounts. Sharing one profile serially is fine; sharing it concurrently is not.
Can an agent re-authenticate on its own?
Only where the site offers a non-interactive path, such as a token endpoint or an API login, and only if you decide an agent should hold those credentials. For a normal password plus second factor, no: automating it either drags credentials into the agent's context or defeats the control the site put there on purpose. Treat re-authentication as a handoff.
Does storageState include session storage and session cookies?
Playwright does not provide an API to persist session storage, and its guide notes that session storage is scoped to a domain and does not survive page loads. The documented workaround is to serialize it yourself in page.evaluate and replay it with an init script, restricted to the intended hostname. This is a real distinction: page-level session storage is transient browser state, closer to the DOM than to your cookies, and it is not what keeps you signed in.
Is it safe to share a session state file with a team?
Treat it as a live credential for the account it belongs to, because that is what it is. Keep it out of version control, out of CI logs, and out of anything an agent reads back into its context, and delete it when it expires. If a teammate needs access, they should establish their own session rather than receive yours.
If the remaining question is which route should create the session in the first place, the connection-route comparison covers DevTools auto-connect, extension injection, CDP takeover, and state copies side by side: connecting an agent to your existing browser.



