ScreenshotNeo

BlogHow-to

How to Manage Browser Sessions in Puppeteer and Playwright

Learn how browser contexts isolate sessions, save and restore Playwright authentication, manage cookies, and handle sessionStorage in Puppeteer and Playwright.

By the ScreenshotNeo team4 October 202611 min read

A browser session in Puppeteer or Playwright is usually a BrowserContext: an isolated browser profile that owns its pages, cookies, and site storage. Put pages that should share a login in the same context; use separate contexts for independent users or tests. Close the context when the session is finished. For repeatable Playwright authentication, save and restore storage state; handle sessionStorage separately because Playwright’s standard storage-state workflow does not persist it.

This guide covers creating and closing sessions, isolating users, saving and reusing authentication, working with cookies, restoring sessionStorage, and diagnosing common problems in both frameworks. The examples use JavaScript and current documented APIs; check the installed package version before copying newer, version-specific storage options.

1. Understand the session boundary

A context is the session boundary in both frameworks. Pages in one context share that context’s browser identity and storage. Pages in separate contexts are isolated from one another. A popup belongs to the context of the page that opened it.

In Playwright, browser.newContext() creates an isolated, non-persistent context. It does not write browsing data to disk. In Puppeteer, use browser.createBrowserContext() for an isolated context. Closing a context closes its pages. Neither framework lets you close the browser’s default context.

Need Use
Pages share one login Create them in the same context.
Independent users or sessions Create a separate context for each identity.
End a short-lived session Close its context.
Reuse Playwright authentication Save and restore storage state.
Preserve a tab-scoped value Capture and restore sessionStorage with a scoped initialization script.

Playwright Test creates a fresh context for each test by default, helping tests avoid carrying browser state into one another. If you create contexts manually, keep the same isolation rule explicit in your test setup. See the official Playwright isolation guide, pages guide, and Puppeteer BrowserContext API.

2. Create, share, and close sessions

Playwright: one session with multiple pages

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

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext();
  try {
    const firstPage = await context.newPage();
    await firstPage.goto('https://example.com');

    // A second page in this context shares its session identity.
    const secondPage = await context.newPage();
    await secondPage.goto('https://example.com/account');
  } finally {
    await context.close(); // closes both pages
    await browser.close();
  }
})();

Puppeteer: one session with multiple pages

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  const context = await browser.createBrowserContext();
  try {
    const firstPage = await context.newPage();
    await firstPage.goto('https://example.com');

    const secondPage = await context.newPage();
    await secondPage.goto('https://example.com/account');
  } finally {
    await context.close();
    await browser.close();
  }
})();

Create a new context for each independent session rather than trying to clear one page and assuming every kind of site state has been removed. Context closure is also a reliable cleanup boundary if navigation or an assertion fails. Keep cleanup in finally when writing standalone scripts.

3. Save and reuse Playwright authentication

Playwright’s authentication workflow lets you sign in once, save browser storage state, and initialize later contexts with that state. This is useful when a test suite needs an authenticated starting point without repeating the interactive login flow for each test.

Save state after logging in

Create a dedicated authentication setup script such as save-auth.js. Adapt the login URL and selectors to the application. The example waits for a post-login URL before saving.

const { chromium } = require('playwright');
const path = require('node:path');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext();
  try {
    const page = await context.newPage();
    await page.goto('https://example.com/login');
    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' }).click();
    await page.waitForURL('**/dashboard');

    const authFile = path.resolve('playwright/.auth/user.json');
    await context.storageState({ path: authFile });
    console.log(`Saved authentication state to ${authFile}`);
  } finally {
    await context.close();
    await browser.close();
  }
})();

Create the directory before running the script, and ensure the state file is ignored by version control. For example:

mkdir -p playwright/.auth
printf '\nplaywright/.auth/\n' >> .gitignore
TEST_EMAIL='test@example.com' TEST_PASSWORD='secret' node save-auth.js

Load state into a new context

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

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    storageState: 'playwright/.auth/user.json'
  });
  try {
    const page = await context.newPage();
    await page.goto('https://example.com/dashboard');
    // Run authenticated work here.
  } finally {
    await context.close();
    await browser.close();
  }
})();

Playwright Test can also use a saved state as the configured state for tests or projects. The key idea is the same: save after the application has completed authentication, then create a fresh context from the saved state.

Know what storage is included

Playwright’s storage-state workflow covers cookies and local storage. Current BrowserContext API documentation also describes IndexedDB, origin private file system entries, and virtual WebAuthn credentials in storage state, with some capabilities controlled by options or package version. If an application keeps its authentication token in IndexedDB, verify the installed Playwright version and enable the documented IndexedDB capture option when supported. The official authentication guide names Firebase Authentication as an example where IndexedDB can matter.

Protect the state file like a password. It may contain cookies or other credentials that allow account impersonation. Keep it out of public and private repositories, logs, shared artifacts, and source control. Prefer a limited test account over a personal or administrator account. Rotate or regenerate the state when the underlying credentials expire or are revoked. See the Playwright authentication guide and BrowserContext API.

4. Manage cookies in both frameworks

Cookies are context-level session data in both tools. Playwright provides cookies(), addCookies(), and clearCookies(). Puppeteer provides context cookie retrieval, setting, and deletion APIs. For new Playwright code, use its context-level cookie methods.

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

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  try {
    await context.addCookies([
      {
        name: 'session_id',
        value: process.env.SESSION_ID,
        url: 'https://example.com',
        httpOnly: true,
        secure: true,
        sameSite: 'Lax'
      }
    ]);

    const page = await context.newPage();
    await page.goto('https://example.com/account');
    const cookies = await context.cookies('https://example.com');
    console.log(cookies.map(cookie => cookie.name));

    // Clear all cookies in this context when appropriate.
    await context.clearCookies();
  } finally {
    await context.close();
    await browser.close();
  }
})();

A cookie can be specified using a URL or a domain-and-path pair. Cookie attributes include expiry, httpOnly, secure, sameSite, and, where relevant, a partition key. Use the attributes expected by the site; for example, an HTTPS-only cookie generally needs the correct secure context.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const context = await browser.createBrowserContext();
  try {
    const page = await context.newPage();
    await page.goto('https://example.com');

    await context.setCookie({
      name: 'session_id',
      value: process.env.SESSION_ID,
      domain: 'example.com',
      path: '/',
      httpOnly: true,
      secure: true
    });

    const cookies = await context.cookies('https://example.com');
    console.log(cookies.map(cookie => cookie.name));
    await context.deleteCookie({ name: 'session_id', domain: 'example.com' });
  } finally {
    await context.close();
    await browser.close();
  }
})();

Cookie APIs and accepted argument details can change with framework releases, so consult the installed version’s reference when adapting cookie records. Playwright’s migration guide maps Puppeteer’s page.setCookie(...cookies) to browserContext.addCookies(cookies), page.cookies([...urls]) to browserContext.cookies([...urls]), and cookie deletion to browserContext.clearCookies(). See the migration guide and Puppeteer cookies guide.

5. Handle sessionStorage explicitly

sessionStorage is scoped to an origin and a browsing session. Playwright’s regular storageState save and restore does not preserve it. If an app depends on values there, read them from the authenticated page and initialize them in the new context before the application scripts run.

Capture sessionStorage

const sessionStorageJson = await page.evaluate(() => {
  const values = {};
  for (let i = 0; i < window.sessionStorage.length; i += 1) {
    const key = window.sessionStorage.key(i);
    values[key] = window.sessionStorage.getItem(key);
  }
  return JSON.stringify(values);
});

// Store this securely alongside any other authentication state.
require('node:fs').writeFileSync(
  'playwright/.auth/session-storage.json',
  sessionStorageJson,
  { mode: 0o600 }
);

Restore it before app code runs

const fs = require('node:fs');
const { chromium } = require('playwright');

(async () => {
  const storage = JSON.parse(
    fs.readFileSync('playwright/.auth/session-storage.json', 'utf8')
  );
  const browser = await chromium.launch();
  const context = await browser.newContext({
    storageState: 'playwright/.auth/user.json'
  });

  await context.addInitScript(({ hostname, values }) => {
    if (window.location.hostname !== hostname) return;
    for (const [key, value] of Object.entries(values)) {
      window.sessionStorage.setItem(key, value);
    }
  }, { hostname: 'example.com', values: storage });

  try {
    const page = await context.newPage();
    await page.goto('https://example.com/dashboard');
    // The initialization script runs before page scripts on this origin.
  } finally {
    await context.close();
    await browser.close();
  }
})();

Replace the hostname and storage shape with the exact origin and values your application uses. Scope the initializer tightly: do not copy authentication values into unrelated sites or subdomains. Treat this file as a secret just like the Playwright auth state.

6. Use separate contexts for parallel users

When a script needs to compare two accounts or perform parallel work as different users, create one context per identity. A shared context would make pages share cookies and could cause one task to change the other task’s login.

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

(async () => {
  const browser = await chromium.launch();
  const [alice, bob] = await Promise.all([
    browser.newContext({ storageState: 'playwright/.auth/alice.json' }),
    browser.newContext({ storageState: 'playwright/.auth/bob.json' })
  ]);

  try {
    const [alicePage, bobPage] = await Promise.all([
      alice.newPage(),
      bob.newPage()
    ]);
    await Promise.all([
      alicePage.goto('https://example.com/dashboard'),
      bobPage.goto('https://example.com/dashboard')
    ]);
  } finally {
    await Promise.all([alice.close(), bob.close()]);
    await browser.close();
  }
})();

Parallelize only as much as the browser host and target application can handle. More contexts increase memory use and concurrent network activity. If sessions must not influence each other, do not share a context just to reduce setup work.

7. Or skip the browser setup

If the goal is to inspect or archive a page rather than operate an authenticated browser session, ScreenshotNeo provides a website screenshot API and MCP server. A single GET request can return a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo 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 like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.

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

8. Troubleshooting browser sessions

Symptom Likely cause Fix
A page in a new context appears logged out Contexts are isolated; the new one has no prior session state. Authenticate in that context or initialize it with saved Playwright storage state and any required extra storage.
One page unexpectedly changes another page’s login The pages share a context and therefore share session identity. Use separate contexts for independent users; retain a shared context only when shared login is intended.
Saved Playwright state does not restore authentication The app may store credentials in IndexedDB or another mechanism rather than only cookies/local storage; state may also have expired. Inspect the app’s storage use, enable documented IndexedDB capture when supported, and regenerate the state after a successful login.
The app still acts logged out after restoring state It may require sessionStorage, which normal storage state omits. Capture it from the authenticated page and restore it with a hostname-scoped addInitScript before navigation.
Cookie is present but not sent Its URL/domain, path, expiry, secure, or same-site attributes do not match the request context. Use the correct origin and cookie attributes; inspect the context’s cookie list for the target URL.
Cannot close a context The code may be trying to close the browser’s default context. Create an explicit context with newContext() or createBrowserContext() and close that context.
State file or API call fails after copying an example The installed framework version may not support a newer option or method. Check the installed package version and its matching official API reference; version-gated features can change.
Tests pass alone but fail in a suite Manually managed contexts may be shared or left open between tests. Use a fresh context per test and close manually created contexts in cleanup, including failure paths.

9. Performance, reliability, and cost considerations

  • Reuse the browser, isolate with contexts. Launching one browser and creating contexts for sessions avoids repeatedly starting browser processes while preserving session boundaries.
  • Close contexts promptly. A context owns its pages and associated state. Leaving contexts open consumes resources and can leak state into later work.
  • Bound concurrency. Each extra context and page adds memory and network load. Choose concurrency based on the host and the target site’s limits.
  • Use deterministic auth setup. Save state only after the application confirms login, and refresh it when it expires. Avoid relying on arbitrary delays when a URL or visible authenticated state can be awaited.
  • Protect credentials. Auth files and cookie values can grant account access. Keep them outside source control and restrict their access.
  • Know when a browser session is unnecessary. If you only need a rendered image or PDF, a screenshot API can avoid maintaining browser processes, login state, and capture cleanup. ScreenshotNeo’s billing rule excludes bot checks, blank pages, failed loads, timeouts, and cache hits; check the response’s verdict and billing headers.

10. Frequently asked questions

Does closing a page end the whole session?

No. Other pages in the same context can remain open. Close the context to close all its pages and end that context’s session.

Can Puppeteer and Playwright use the same saved authentication file?

The documented general storage-state save-and-restore workflow in the sources here is Playwright’s. Puppeteer contexts support isolation and cookie operations, but these sources do not establish an equivalent general storage-state API. Do not assume a Playwright state file is directly interchangeable with Puppeteer.

Are cookies enough to restore every login?

Not always. Authentication may depend on local storage, IndexedDB, sessionStorage, or WebAuthn credentials. Identify which mechanisms the application uses before deciding what needs to be saved.

Should authentication state be committed to a private repository?

No. Treat it like a password or bearer token and exclude it from source control, including private repositories.

Official references