How to Save and Reuse Browser Sessions in Playwright
Save Playwright authentication state after login, reuse it in tests, and choose a safe setup for parallel workers, multiple roles, and different storage types.
Save an authenticated browser session with await page.context().storageState({ path: authFile }) after login has fully completed. Reuse it by passing the saved file as storageState when creating a browser context or in a Playwright Test project’s use configuration. This avoids repeating the login flow while retaining a fresh, isolated browser context for each test.
Authentication state is sensitive: it can contain cookies and tokens that let someone impersonate the account. Store it outside version control, restrict access, and regenerate it when it expires. The examples below use Playwright Test with JavaScript. See the official authentication guide and BrowserContext API.
1. Save state after login
Install Playwright Test if your project does not already use it, then create a setup test that signs in once and writes the state file. Wait for a reliable signal that authentication has completed, such as the final URL or a visible account control. Saving immediately after clicking “Sign in” can capture a partially established session.
// tests/auth.setup.js
import { test as setup, expect } from '@playwright/test';
import path from 'node:path';
const authFile = path.join('playwright', '.auth', 'user.json');
setup('authenticate', async ({ page }) => {
await page.goto('https://your-app.example/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();
// Prefer an authenticated UI marker or a stable final URL.
await expect(page.getByRole('button', { name: 'Account' })).toBeVisible();
await page.context().storageState({ path: authFile });
});
Create the output directory before the setup runs and ignore it in Git. For example:
mkdir -p playwright/.auth
printf '\nplaywright/.auth/\n' >> .gitignore
Use environment variables or your CI secret store for credentials. Do not put passwords in the test file, and do not commit the generated state file. The saved file is a credential even if it does not contain the original password.
2. Reuse state in Playwright Test
Configure an authentication setup project to run before the browser test project. Playwright will create a new context for each test and initialize it from the saved state.
// playwright.config.js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
projects: [
{
name: 'setup',
testMatch: /auth\.setup\.js/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Then tests can navigate directly to authenticated pages:
// tests/account.spec.js
import { test, expect } from '@playwright/test';
test('shows the signed-in account', async ({ page }) => {
await page.goto('https://your-app.example/account');
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});
Run the suite with npx playwright test. If the state expires, rerun the setup project to log in and write fresh state. Playwright’s UI mode does not run setup projects by default; when authentication expires, run the setup explicitly before relying on the dependent tests.
3. Pick the right reuse pattern
One shared account
A single state file and account are convenient when tests can run concurrently without interfering with each other’s server-side data. This is a poor fit if tests change shared records, invalidate sessions, or depend on browser-specific authentication. Parallel tests may overwrite or delete each other’s data even though their browser contexts are isolated.
One account per parallel worker
When tests mutate shared server-side state, assign each parallel worker a distinct account and state file. Playwright’s parallelIndex is a useful key. Arrange for a unique test account per worker; also avoid collisions with other local runs or team members.
// tests/worker-auth.setup.js
import { test as setup } from '@playwright/test';
import path from 'node:path';
setup('authenticate worker', async ({ page }, testInfo) => {
const workerIndex = testInfo.parallelIndex;
const authFile = path.join('playwright', '.auth', `worker-${workerIndex}.json`);
const email = process.env[`TEST_USER_${workerIndex}_EMAIL`];
const password = process.env[`TEST_USER_${workerIndex}_PASSWORD`];
if (!email || !password) {
throw new Error(`Missing credentials for worker ${workerIndex}`);
}
await page.goto('https://your-app.example/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByRole('button', { name: 'Account' }).waitFor();
await page.context().storageState({ path: authFile });
});
For the full worker-fixture pattern, see the official per-worker authentication example. Keep project configuration and fixture paths aligned so every worker loads the state it generated.
Multiple roles
Save a different state file for each role, such as admin.json and member.json, and select the relevant file for each test or project. If a single test needs two signed-in users interacting, create two browser contexts, each with its own storageState, and use a page from each context. Do not share one context when the test is meant to represent separate browser sessions.
API-based login
If the application provides a suitable authentication endpoint, you can authenticate with Playwright’s API request context and save the resulting state rather than driving the login UI. This can make setup less dependent on login page rendering. The endpoint, request body, and response behavior are application-specific; use the app’s supported test authentication flow and avoid bypassing production controls.
Ephemeral state per run
If state must not persist between test runs, write it under Playwright’s project outputDir. Playwright cleans that directory before each run. This means the setup project must regenerate the file as part of the run; configure dependent projects to wait for setup.
4. Understand what storage state contains
A normal storage-state snapshot is designed to restore authentication data such as cookies and local storage. Current Playwright APIs can also cover IndexedDB, passkey-related virtual WebAuthn credentials, and origin private file system (OPFS) data, but support and options depend on the Playwright version and browser. Check the API reference for the version installed in your project.
| Storage or credential | What to check |
|---|---|
| Cookies and local storage | Commonly used by saved authentication state. Confirm the app’s origin and cookie scope match the pages under test. |
| IndexedDB | If the app stores its auth token there, enable the storage-state IndexedDB option where supported. The option was added in Playwright v1.51. |
| Session storage | It is origin-specific and does not persist across page loads. The authentication guide documents a workaround using addInitScript; a normal state file should not be assumed to restore it. |
| WebAuthn passkeys | Virtual WebAuthn credentials have API support in newer versions. Verify the installed version and target browser. |
| OPFS | Snapshot support is version-specific (documented as added in v1.63); OPFS is not supported in ephemeral WebKit contexts. |
Newer BrowserContext methods such as setStorageState and WebStorage API methods also have version requirements. The API reference lists introduction versions; check the installed package with npx playwright --version before copying newer examples.
Persisting sessionStorage with an init script
The documented workaround is to read session storage from the page, serialize it, and restore it before application code runs for the matching hostname. Treat this data as sensitive too.
// During authenticated setup, after login has completed:
const sessionStorageData = await page.evaluate(() => {
const data = {};
for (let i = 0; i < window.sessionStorage.length; i++) {
const key = window.sessionStorage.key(i);
data[key] = window.sessionStorage.getItem(key);
}
return data;
});
// Save sessionStorageData alongside the state file using your project's
// chosen secure serialization mechanism. Load it before creating test pages.
// In a test fixture, before navigation to the app:
await context.addInitScript(({ hostname, data }) => {
if (window.location.hostname !== hostname) return;
for (const [key, value] of Object.entries(data)) {
window.sessionStorage.setItem(key, value);
}
}, { hostname: 'your-app.example', data: sessionStorageData });
Keep the hostname check: session storage belongs to an origin, and injecting it into unrelated pages would be incorrect. Adapt serialization to the application’s origin and security requirements.
5. Troubleshooting and edge cases
| Symptom | Likely cause | Fix |
|---|---|---|
| Test redirects to login | State was saved before login finished, expired, or is scoped to another origin. | Wait for an authenticated UI marker or final URL before saving; inspect cookie domains and origins; regenerate state. |
| State file is missing | The setup did not run, the path differs from the configured path, or its parent directory does not exist. | Create the parent directory, confirm setup project dependency and paths, and explicitly run setup in UI mode when needed. |
| Only some tests fail in parallel | Workers share a mutable account or server-side records. | Use a distinct account and state file per worker, or serialize tests that must share state. |
| Works in Chromium but not another browser | Authentication may be browser-specific, or the storage mechanism differs. | Generate state for the target browser and verify the app’s auth behavior per browser. |
| App still appears signed out despite a state file | Auth may live in sessionStorage, IndexedDB, or another storage mechanism not captured by the defaults/options used. | Identify where the application stores its token; enable supported IndexedDB capture or use the sessionStorage init-script workaround. |
| Newer API option is rejected | Installed Playwright version predates that option, or the browser context does not support it. | Check the API’s “Added in” version and browser notes; upgrade deliberately or use a supported storage strategy. |
| CI login is flaky | Redirects, MFA, or page rendering may not have completed when state is captured. | Use a stable authenticated locator or final URL, keep setup diagnostics, and make credentials available through CI secrets. |
| Repository accidentally contains auth state | The generated file was not ignored or was committed before the ignore rule. | Remove it from tracking, rotate or invalidate the session if appropriate, add it to ignore rules, and restrict artifact access. |
6. Reliability, speed, and cost
Reusing state saves repeated login navigation and reduces dependence on the login UI, but it does not make a session permanent. State can expire, be revoked, or become invalid when the app changes its authentication policy. Keep setup as a first-class part of the test run and fail clearly when the authenticated marker never appears.
Parallelism is safe only when the account and application data can tolerate concurrent use. Browser contexts isolate cookies and local storage between tests, but they do not isolate server-side account data. Unique worker accounts reduce collisions when tests create, update, or delete shared records.
The cost is operational: maintain test accounts, protect state files, and refresh state when needed. For tests that validate the login flow itself, do not bypass that flow with pre-saved state; keep a dedicated login test. Use saved sessions for tests whose subject is functionality after authentication.
7. Or skip the browser setup
If the goal is to capture a page rather than test an authenticated workflow, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF. See the API documentation for options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets Claude, Cursor, and other MCP clients take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
8. FAQ
Does storageState save my password?
It saves browser authentication state, which may include cookies or tokens that grant account access. Protect it as a credential even when the password itself is absent.
Should every test use the same saved session?
Only if concurrent tests can safely share the same account and server-side data. Use per-worker accounts when tests mutate shared state.
Can I reuse state across browsers?
Sometimes, but do not assume it. Authentication can depend on browser-specific cookies or behavior; generate and validate state for each target browser.
Will saved state test the login flow?
No. It starts the test already authenticated. Keep a separate test that performs login when validating that flow.


