ScreenshotNeo

BlogHow-to

How to Persist Browser Profiles Without Breaking Web Automation

Keep Playwright logins across runs with persistent profiles or saved authentication state, while avoiding profile conflicts and flaky parallel tests.

By the ScreenshotNeo team4 October 202610 min read

Direct answer: To keep browser state on disk across restarts, launch Playwright with launchPersistentContext(userDataDir) and a dedicated automation profile directory. Only one browser instance can use that directory at a time. For repeatable tests that need authentication but should use fresh contexts, save and reuse Playwright storageState instead. Never automate against your everyday Chrome profile.

A persistent profile and a saved authentication snapshot are related but not interchangeable. A profile retains browser state in its user-data directory. A storageState file is a deliberate snapshot used to initialize another context; it does not automatically include sessionStorage. Choose based on what your automation needs to preserve and how it will run concurrently.

1. Choose the right kind of persistence

Approach Use it when Tradeoffs
Persistent browser profile A long-running or manually operated automation session needs browser profile state to survive browser restarts. The directory belongs to one browser instance at a time. It is mutable and less convenient to share between parallel tests.
Saved storageState Tests need a known authenticated starting point in fresh contexts. It is an authentication-state snapshot, not a complete copy of every browser storage API. In particular, Playwright does not automatically persist sessionStorage.
Fresh unauthenticated context The test is about sign-in itself, or authentication should not affect the scenario. Each run must perform the login flow or use another explicit test setup.

Playwright’s persistent context uses the supplied user-data directory and returns the browser’s only context. Closing that context closes the browser. Playwright warns that multiple browser instances cannot launch with the same user-data directory and that automating Chrome’s default user profile is unsupported under recent Chrome policy changes. See the [Playwright BrowserType API](https://playwright.dev/docs/api/class-browsertype).

2. Persist a dedicated profile across runs

Use a directory reserved for automation. Keep it separate from Chrome’s everyday profile, and make sure no other browser process is using it. The example below uses Playwright for Node.js with Chromium.

Install Playwright

npm init -y
npm install playwright
npx playwright install chromium

Launch with a persistent context

Save this as persistent-profile.mjs. The first run can sign in interactively; later runs open the same dedicated profile and can reuse its persisted browser state.

import { chromium } from 'playwright';
import path from 'node:path';

const userDataDir = path.resolve('./.playwright-profile');
const context = await chromium.launchPersistentContext(userDataDir, {
  headless: false,
  viewport: { width: 1440, height: 900 },
});

try {
  const page = context.pages()[0] ?? await context.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  // On the first run, complete the site's login in the opened browser.
  // On later runs, the same dedicated profile may retain its browser state.
  console.log('Current page:', page.url());
  await page.waitForTimeout(5000);
} finally {
  // Closing the persistent context also closes its browser.
  await context.close();
}

Run it with node persistent-profile.mjs. Add .playwright-profile/ to your repository’s .gitignore. Do not delete the directory between runs if you intend to retain its state.

Persistent-context options

launchPersistentContext(userDataDir, options) accepts the browser launch options plus context options. Select only the settings your workflow needs.

Option or choice What it affects Practical guidance
userDataDir Where the persistent profile is stored. Use an absolute or well-understood path dedicated to automation. A relative path depends on the process working directory.
headless Whether the browser has a visible window. Use false for interactive login and debugging; use the appropriate headless setting for unattended runs.
viewport The page’s emulated viewport dimensions. Set explicitly when layout-dependent actions or screenshots need consistent dimensions.
locale, timezoneId, colorScheme Context environment seen by pages. Keep values consistent between authentication setup and automation when the application varies by locale, time zone, or theme.
executablePath Which browser executable is launched. Prefer Playwright’s managed browser unless you have a specific compatible-browser requirement. Avoid pointing at the everyday Chrome profile.
args Additional browser command-line arguments. Use sparingly; custom flags can change browser behavior and make runs harder to reproduce.
ignoreDefaultArgs Whether Playwright’s normal browser arguments are omitted. Do not change without a concrete need; it can interfere with Playwright’s expected browser operation.

Check the [launchPersistentContext API reference](https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context) for the current option set and browser-specific details.

3. Use saved authentication state for isolated tests

For test suites, a common pattern is to authenticate once, save storage state, then create independent contexts seeded from that state. This makes the starting authentication state explicit while keeping each test’s mutable browser context separate.

Save state after signing in

Create save-auth.mjs and complete the site’s login before the state file is written. The file contains sensitive login-related state.

import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';

await mkdir('./playwright/.auth', { recursive: true });
const browser = await chromium.launch();
const context = await browser.newContext();

try {
  const page = await context.newPage();
  await page.goto('https://example.com/login');

  // Complete the login flow for your application here.
  // For a manual login, wait while you sign in in a visible browser instead.
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL ?? '');
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD ?? '');
  await page.getByRole('button', { name: /sign in/i }).click();
  await page.waitForURL('**/dashboard');

  await context.storageState({ path: './playwright/.auth/user.json' });
} finally {
  await browser.close();
}

Adapt the login locators and completion condition to the application. Do not put real credentials directly in source code; supply them through your secret-management mechanism. Add playwright/.auth/ to .gitignore.

Seed a fresh context from the saved state

import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  const context = await browser.newContext({
    storageState: './playwright/.auth/user.json',
  });
  const page = await context.newPage();
  await page.goto('https://example.com/dashboard');
  console.log('Dashboard loaded:', page.url());
  await context.close();
} finally {
  await browser.close();
}

For parallel tests, give each test or worker its own context. A shared read-only starting snapshot can be appropriate when tests use the same test account and the application allows it, but do not have simultaneous browser processes mutate the same persistent profile directory. Playwright recommends isolating cookies and storage to improve reproducibility and avoid cascading failures. See [Playwright Best Practices](https://playwright.dev/docs/best-practices) and [Playwright Authentication](https://playwright.dev/docs/auth).

4. Handle sessionStorage explicitly when needed

Playwright’s built-in storageState does not automatically persist sessionStorage. If the application puts required authentication data there, a state file may appear valid while the new context is still logged out.

Capture only the keys your application requires, and restore them before the app’s scripts run. One approach is to save the value from an authenticated page and inject an initialization script into each new context:

// In the authenticated page, collect the app-specific values:
const sessionValues = await page.evaluate(() => ({
  accessToken: sessionStorage.getItem('accessToken'),
}));

// Persist sessionValues securely using your test setup.

// Before navigating to the application in a new context:
await context.addInitScript((values) => {
  if (location.origin === 'https://example.com' && values.accessToken) {
    sessionStorage.setItem('accessToken', values.accessToken);
  }
}, sessionValues);

await page.goto('https://example.com');

This is an application-specific example, not a generic session export format. Restrict the origin, avoid copying unrelated keys, and protect the captured values like credentials. Confirm that the app’s authentication flow permits restoring these values; some applications bind sessions to additional state or expire them server-side.

5. Run parallel workers without profile collisions

  1. Decide what is shared. Share a deliberately created authentication snapshot when appropriate; keep each worker’s live context and mutable test data independent.
  2. Use a separate directory per persistent browser. If workers need persistent profiles, derive a unique directory from a stable worker identifier, such as .profiles/worker-0. Ensure two processes cannot receive the same identifier.
  3. Do not launch two browsers against one directory. A lock file is not a coordination strategy; serialize access or assign different directories.
  4. Clean up deliberately. Remove disposable worker profiles after the job, but retain a dedicated profile only when its persistence is intentional.
  5. Separate test accounts or data when needed. Independent browser contexts do not prevent two tests from changing the same server-side account or record.

When test isolation matters more than preserving an entire browser profile, use fresh contexts with saved authentication state. Playwright’s guidance emphasizes independent storage and cookies; see the [authentication guide](https://playwright.dev/docs/auth).

6. Protect profile and authentication files

  • Keep profile directories and authentication-state JSON files out of public repositories and build artifacts.
  • Limit filesystem permissions to the user or job that needs the state.
  • Use dedicated test accounts instead of copying a personal profile.
  • Rotate or recreate authentication state when credentials or access are revoked.
  • Do not print cookie values, tokens, or profile contents into CI logs.

Profiles and exported state can contain credentials or other sensitive browser data. A 2025 paper examines browser-profile security risks; see [User Profiles: The Achilles’ Heel of Web Browsers](https://arxiv.org/abs/2502.XXXXX). Treat the profile as sensitive even when the stored files do not look like a password file.

7. Troubleshoot common failures

Symptom Likely cause Fix
Chrome exits immediately or refuses to start with a profile path. The path points to Chrome’s everyday default profile, or recent Chrome policy prevents that automation use. Create a separate automation directory and launch Playwright with that directory. Do not target the ordinary profile.
Profile lock error or launch fails only when jobs overlap. Another browser instance is using the same user-data directory. Ensure exclusive access, serialize those launches, or give each process a distinct directory.
Login works in the setup run but disappears in another test. The app relies on sessionStorage, the state file was not loaded, or the session expired or was revoked. Verify the storageState path and origin, inspect the app’s storage requirements, explicitly save and restore needed session storage, and reauthenticate if the server session expired.
Tests pass alone but fail in parallel. Workers share mutable browser state or conflict over server-side test data. Use separate contexts and, where persistent profiles are necessary, separate directories. Isolate test accounts or records too.
State file is missing or empty. The save step ran before login completed, the output directory does not exist, or the process wrote to a different working directory. Wait for a reliable post-login condition, create the parent directory, and resolve the output path explicitly.
Browser opens but the app still redirects to sign-in. The app may require session storage, additional authentication steps, or a still-valid server-side session. Check cookies and local storage in the intended origin, complete the full login flow, handle session storage explicitly, and refresh expired credentials.
Different layout or behavior between setup and test. Locale, time zone, viewport, browser version, or headless mode differs. Use consistent context options and browser versions for state creation and consumption.

8. Performance, reliability, and cost

A persistent profile avoids repeating the full browser setup and may preserve the state your workflow needs, but the profile is mutable and exclusive to one running browser. Its size can grow over time, and stale cookies or application data can make failures harder to reproduce. Use a clean profile when diagnosing state-related bugs.

A saved authentication snapshot makes test setup explicit and works naturally with fresh contexts, but it may need regeneration when sessions expire or the application changes its login behavior. It also omits session storage unless you add application-specific handling. Neither approach prevents server-side session expiry, account lockouts, or test-data conflicts.

There is no universal cost or speed winner: compare the cost of repeated login/setup with the maintenance cost of keeping profiles safe and isolated. For CI, account for storage retention and cleanup, and avoid uploading secrets as ordinary artifacts. No generic profile-persistence benchmark applies across applications and environments.

9. Or skip the browser setup

If your goal is to capture a page rather than interact with an authenticated application, [ScreenshotNeo](https://screenshotneo.com) provides a website screenshot API and MCP server. A single GET request can return PNG, JPEG, WebP, or PDF. See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for parameters and formats.

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}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, with no card required.

10. FAQ

Does launchPersistentContext return a Browser object?

No. It returns the persistent BrowserContext, which is the browser’s only context. Closing it closes the browser.

Can I use one persistent profile for multiple Playwright workers?

Not at the same time. Give each simultaneously running browser its own user-data directory, or use separate contexts initialized from authentication state.

Does saving storageState keep me logged in forever?

No. The snapshot can seed a context, but the application or identity provider may expire or revoke the server-side session.

Should I commit the auth JSON file?

No. Treat it as a credential and keep it out of source control and public artifacts.