ScreenshotNeo

BlogHow-to

Why does Playwright lose login cookies between screenshot runs?

Playwright usually loses login cookies because each screenshot run starts a fresh browser context. Save and load authentication state to reuse a login.

By the ScreenshotNeo team4 October 20269 min read

Playwright usually loses login cookies because each screenshot run creates a new, isolated browser context. A new context starts without the cookies and other browser storage from the previous run. Save authentication state after login, then load it into the context used for the next screenshot. For most repeatable screenshot jobs, Playwright’s storageState is the right starting point.

The key distinction is between a browser process and a browser context. A browser can stay open while a newly created context still has a separate, empty session. Playwright documents ordinary browser.newContext() contexts as isolated and non-persistent: they do not write browsing data to disk or share it with another context. Playwright authentication explains how to save and reuse authentication state; the BrowserContext API describes context behavior.

What is happening to the login?

Cookies belong to a browser context and are scoped by attributes such as domain, path, and expiration. When a login flow sets cookies in context A, a screenshot in newly created context B will not see them unless you explicitly initialize B with saved state. Closing the browser or process can also discard data held only in memory.

Authentication may also depend on local storage, IndexedDB, or session storage rather than cookies alone. A saved cookie file cannot restore data it never captured. First establish which storage mechanism the application actually uses, then choose the matching persistence approach.

  1. Run the login flow in a context dedicated to creating authentication state.
  2. Wait until the login is complete. Verify a final URL or a reliable signed-in element; do not save state midway through a redirect or multi-step login.
  3. Save the context’s state to a file that the later screenshot process can read.
  4. Create the screenshot context with that same file as its storageState.
  5. Check the resulting page is authenticated before relying on the screenshot.

Runnable JavaScript example

This example uses Playwright’s library API. Set LOGIN_URL, SCREENSHOT_URL, and the login selectors to match your application. Provide credentials through environment variables rather than committing them to source control.

const { chromium } = require('playwright');

async function main() {
  const browser = await chromium.launch({ headless: true });

  // Create state once, after a successful interactive login.
  const loginContext = await browser.newContext();
  const loginPage = await loginContext.newPage();
  await loginPage.goto(process.env.LOGIN_URL, { waitUntil: 'domcontentloaded' });
  await loginPage.locator('[name="email"]').fill(process.env.LOGIN_EMAIL);
  await loginPage.locator('[name="password"]').fill(process.env.LOGIN_PASSWORD);
  await loginPage.locator('button[type="submit"]').click();
  await loginPage.waitForURL(url => url.pathname.includes('/dashboard'));
  await loginPage.getByRole('heading', { name: 'Dashboard' }).waitFor();
  await loginContext.storageState({ path: 'playwright/.auth/user.json' });
  await loginContext.close();

  // A later screenshot context starts fresh, then imports the saved state.
  const screenshotContext = await browser.newContext({
    storageState: 'playwright/.auth/user.json'
  });
  const page = await screenshotContext.newPage();
  await page.goto(process.env.SCREENSHOT_URL, { waitUntil: 'networkidle' });
  await page.getByRole('heading', { name: 'Account' }).waitFor();
  await page.screenshot({ path: 'account.png', fullPage: true });

  await screenshotContext.close();
  await browser.close();
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

Install Playwright for this project with npm install -D playwright, then run the script with the environment variables set. Replace the sample selectors, redirect condition, and signed-in indicator with ones that are stable for your site. Create the playwright/.auth directory before saving if it does not exist.

Playwright Test configuration

With Playwright Test, you can load the state for a project or for an individual test. Save the state in a setup step, and make sure the screenshot project depends on that setup if it needs the file first. The path is resolved relative to the test configuration or process working directory according to how you pass it, so keep the producer and consumer paths consistent.

// playwright.config.js
const { defineConfig } = require('@playwright/test');

module.exports = defineConfig({
  use: {
    storageState: 'playwright/.auth/user.json'
  }
});

To create a reusable state file from a setup script, authenticate and call await page.context().storageState({ path: 'playwright/.auth/user.json' }) only after the signed-in state is confirmed. If the setup runs in CI, ensure the screenshot job can access the generated file, or generate it in the same job.

Choose the right persistence model

Approach Use it when What to verify
Fresh context with saved storageState You want repeatable, isolated test or screenshot runs. The correct state file is loaded, and it contains the storage your app needs.
Persistent context with a user data directory The workflow intentionally depends on a browser profile persisting across launches. The profile directory is stable and is not opened concurrently by multiple browser instances.
Manual session storage restore The application keeps authentication only in sessionStorage. The restore script runs before application code reads the session value.

For test automation, Playwright documents saved storage state as the way to reuse authentication in fresh contexts. A persistent context launched with a user data directory is a different option when profile-level persistence is required. See the official authentication guide and launchPersistentContext API.

Storage coverage and edge cases

Cookies and local storage

Storage state is commonly sufficient for cookie-based sessions and local-storage tokens. Confirm that the file contains the expected origin and cookie names, but never print cookie values into logs or share the file as a build artifact. Authentication state can contain credentials that allow someone to impersonate the account.

IndexedDB

If the app stores its authentication token in IndexedDB, use the IndexedDB capture option supported by the Playwright version installed in your project. The current BrowserContext API supports requesting IndexedDB in a state snapshot. Check your installed version’s API because this option may not exist in older versions.

await page.context().storageState({
  path: 'playwright/.auth/user.json',
  indexedDB: true
});

Session storage

Playwright’s authentication guide says it does not provide an API to persist sessionStorage. If your app depends on it, capture the relevant keys yourself and restore them with an init script that runs before the page’s application code. Keep the script narrowly scoped to the trusted origin and avoid logging values.

// Save only the keys your application requires, after login.
const sessionData = await page.evaluate(() => ({
  accessToken: sessionStorage.getItem('access-token')
}));
require('fs').writeFileSync(
  'playwright/.auth/session.json',
  JSON.stringify(sessionData),
  { mode: 0o600 }
);

// Before navigation in the later context, restore for the intended origin.
const sessionData = require('./playwright/.auth/session.json');
await context.addInitScript(({ origin, values }) => {
  if (location.origin === origin && values.accessToken) {
    sessionStorage.setItem('access-token', values.accessToken);
  }
}, { origin: 'https://app.example.test', values: sessionData });

Run addInitScript before navigating to the application. Adapt the key names and origin to your app, and protect the resulting file as you would any authentication secret. The official guide includes a manual session storage save and load pattern: Playwright session storage guidance.

  • Domain and path: A cookie for one subdomain or path may not be sent to the screenshot URL. Check the target hostname and cookie scope.
  • Secure and SameSite: A cookie’s attributes can affect whether the browser sends it in the request context you are using, particularly across origins or navigation flows.
  • Expiry: Session cookies or expired cookies may no longer provide a login after the relevant browser session ends or enough time passes.
  • Server-side revocation: A valid-looking saved cookie can be rejected if the server has expired, revoked, or rotated the session. Sign in again and refresh the state file.
  • Environment mismatch: A state file for staging may not authenticate against production, and the reverse is also true.

Troubleshooting checklist

Symptom Likely cause Fix
Every screenshot run redirects to login The new context does not load state, or the state path is wrong. Log the resolved path (not state contents), confirm the file exists, and pass it to browser.newContext({ storageState: path }).
Login works locally but fails in CI The state file was created in another job, workspace, or project directory. Generate state in the same job or securely transfer it; use a stable explicit path and check file permissions.
State file exists but user is logged out State was saved before login completed, or a redirect flow had not finished. Wait for the final URL and a signed-in UI indicator before calling storageState().
Some pages are logged in, others are not Cookie scope, origin, or storage requirements differ across pages. Check target origin and cookie domain/path; determine whether the app uses IndexedDB or session storage too.
It works until the next day The server session expired or the cookie expired. Refresh authentication state with a supported login flow; do not assume saved state is permanent.
Browser profile is locked or launch fails Two processes are using the same persistent user data directory. Use one process per profile directory, or prefer separate fresh contexts initialized from storage state.
Authentication appears in one browser but not another State was created for a different browser profile or incompatible target origin. Regenerate and consume the state in a consistent environment and verify the target host.

For a safe diagnosis, log the context creation path, resolved state-file path, target origin, and whether the signed-in indicator appeared. Do not log cookie values, authorization headers, or the full state JSON.

Security, reliability, and cost considerations

Protect authentication state

Treat the state file like a password. Add generated auth files to .gitignore, restrict filesystem permissions, avoid uploading them to public CI artifacts, and use a dedicated low-privilege test account. Rotate or recreate the state if it may have been exposed. Playwright explicitly warns that stored authentication state can contain cookies and headers that can impersonate an account; see its authentication guidance.

Make screenshot runs deterministic

Use an explicit login completion condition rather than a fixed sleep. Reuse state only while it remains valid, and make state creation a clear prerequisite of screenshot jobs. A fresh isolated context per run gives cleaner separation between runs, while an intentional persistent profile can retain unrelated browsing data and make results harder to reproduce.

Account for browser work and session refreshes

Running a browser means provisioning the Playwright browser binaries, launching a browser, waiting for navigation, and managing state files. Login pages may trigger MFA, rate limits, or bot controls; automate only flows permitted by your application and account. If sessions expire frequently, include a supported reauthentication step rather than endlessly retrying screenshots with stale state.

Or skip the browser setup

If you need a clean screenshot of a public page rather than an authenticated private page, ScreenshotNeo provides a screenshot API and MCP server. It does not reuse your Playwright login cookies for private pages. For a public URL, one request returns an image or PDF; see the API documentation.

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 accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. These public-page captures do not replace authenticated Playwright state for private pages.

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

FAQ

Does keeping the same browser process keep me logged in?

Not across newly created contexts. The login state belongs to the context unless you explicitly reuse or persist it.

Should I save a new state file for every test?

Usually a shared state is fine for read-only tests. Use separate accounts or state files when tests mutate account data or need isolation.

Can ScreenshotNeo capture a page using my Playwright cookies?

The API examples here take a URL and access key; they do not transfer a Playwright context’s private login state. Use Playwright for authenticated pages that require your browser session.