ScreenshotNeo

BlogHow-to

How to Preserve Session IDs Across Puppeteer Page Navigations

Keep Puppeteer authentication intact across navigations, pages, browser contexts, cookies, redirects, and storage boundaries.

By the ScreenshotNeo team1 October 20268 min read

Direct answer: Store the server-recognized session ID in a cookie scoped to the target origin, keep related pages in the same Puppeteer BrowserContext, and inspect or restore cookies with browser or context cookie APIs. A new context is isolated by design, so copy state explicitly when you intentionally cross that boundary.

1. The reliable session-preservation pattern

After a successful login, read the application cookies from the same browser context that owns the page. Navigate within that context and the browser will send matching cookies automatically. If you need an isolated context, transfer only the cookies required by the application before opening the destination page.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const context = await browser.createBrowserContext();
const page = await context.newPage();

await page.goto('https://app.example.com/login', {
  waitUntil: 'networkidle2'
});

await page.locator('input[name="email"]').fill(process.env.APP_EMAIL);
await page.locator('input[name="password"]').fill(process.env.APP_PASSWORD);
await page.locator('button[type="submit"]').click();
await page.waitForNavigation({waitUntil: 'networkidle2'});

// Read cookies at browser-context level after login.
const authCookies = await context.cookies('https://app.example.com');
console.log(authCookies.map(({name, domain, path, expires}) => ({
  name, domain, path, expires
})));

// Same context: matching cookies are sent automatically.
await page.goto('https://app.example.com/account', {
  waitUntil: 'networkidle2'
});

// New context: restore state explicitly.
const isolated = await browser.createBrowserContext();
await isolated.setCookie(...authCookies);
const secondPage = await isolated.newPage();
await secondPage.goto('https://app.example.com/account', {
  waitUntil: 'networkidle2'
});

await browser.close();

Puppeteer documents cookie operations at browser or browser-context level; page-level cookie methods are deprecated in the current API reference. See the Puppeteer cookies guide.

2. Why a session ID disappears

Situation What happens What to do
Same page, same context Cookies matching the request remain available. Check redirects and cookie scope.
Second page in the same context Cookies and local storage are shared according to normal origin rules. Create the page from the existing context.
New BrowserContext Cookies and local storage are isolated. Transfer cookies explicitly with setCookie.
Different origin or domain The original cookie may not match the request. Use the cookie’s domain, path, secure, and same-site rules as the source of truth.
Session storage State is origin and tab scoped. Export and restore it deliberately; do not assume a new page inherits it.

The cookie name alone does not determine whether it is sent. Preserve the server-issued value and attributes:

  • Domain: A host-only cookie belongs to the exact host. A parent-domain cookie may cover subdomains when the server set it that way.
  • Path: A cookie set for /account is not necessarily sent to unrelated paths.
  • Secure: Secure cookies are sent over HTTPS, not plain HTTP.
  • SameSite: Cross-site requests can be restricted by the cookie’s same-site policy.
  • Expiry: An expired token will not authenticate a later navigation.
  • HttpOnly: JavaScript cannot read this cookie, but the browser can still send it with requests.

Do not copy a live authentication cookie to an unrelated domain or alter its value. The browser enforces scope, and the server can reject an expired or modified token.

4. Inspect state before and after navigation

async function logCookies(context, url, label) {
  const cookies = await context.cookies(url);
  console.log(label, cookies.map(cookie => ({
    name: cookie.name,
    domain: cookie.domain,
    path: cookie.path,
    expires: cookie.expires,
    secure: cookie.secure,
    httpOnly: cookie.httpOnly,
    sameSite: cookie.sameSite
  })));
}

await logCookies(context, 'https://app.example.com', 'after login');
await page.goto('https://app.example.com/account', {waitUntil: 'domcontentloaded'});
await logCookies(context, 'https://app.example.com', 'after account navigation');

Logging names and attributes is useful for debugging. Avoid writing token values to source control, CI logs, screenshots, or test artifacts.

5. Seed state before every navigation

page.evaluateOnNewDocument() runs after a document is created and before that document’s scripts execute on each navigation. Use it for non-cookie state such as application values stored in web storage. It does not replace a server-issued authentication cookie.

const sessionId = process.env.SESSION_ID;

await page.evaluateOnNewDocument((id) => {
  // Use this only when the application genuinely stores the value here.
  window.localStorage.setItem('session_id', id);
}, sessionId);

await page.goto('https://app.example.com/account', {
  waitUntil: 'networkidle2'
});

Install the hook before the navigation that needs the value. If the application uses session storage, restore it in the page that owns that tab and origin:

const storage = {session_id: process.env.SESSION_ID};
await page.evaluateOnNewDocument((entries) => {
  for (const [key, value] of Object.entries(entries)) {
    window.sessionStorage.setItem(key, value);
  }
}, storage);

6. Moving authentication to another context

A fresh context is useful for isolation, parallel tests, or a clean browser profile. It will not inherit cookies or local storage. Transfer only the required cookies, then create the page.

const sourceCookies = await context.cookies('https://app.example.com');
const isolated = await browser.createBrowserContext();
await isolated.setCookie(...sourceCookies);
const page2 = await isolated.newPage();
await page2.goto('https://app.example.com/account', {
  waitUntil: 'networkidle2'
});

If authentication spans multiple subdomains, inspect cookies for each relevant origin and verify the cookie domain. A redirect to another origin can change which cookies apply; follow the final URL before deciding that the session was lost.

import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const context = await browser.createBrowserContext();
const page = await context.newPage();

await page.goto('https://app.example.com/login', {waitUntil: 'networkidle2'});
await page.locator('input[name="email"]').fill(process.env.APP_EMAIL);
await page.locator('input[name="password"]').fill(process.env.APP_PASSWORD);
await page.locator('button[type="submit"]').click();
await page.waitForNavigation({waitUntil: 'networkidle2'});

const cookies = await context.cookies('https://app.example.com');
await fs.writeFile('/tmp/app-cookies.json', JSON.stringify(cookies, null, 2), {
  mode: 0o600
});

await page.goto('https://app.example.com/account', {waitUntil: 'networkidle2'});
console.log('Final URL:', page.url());
console.log('Title:', await page.title());

await browser.close();

Use a protected secret store for production credentials and cookie jars. Treat the saved file as an authentication secret.

8. Troubleshooting

“I logged in, but page.goto() shows the login page

Log cookies immediately after login and after navigation. Confirm that the login request completed, the cookie has not expired, and the destination URL matches its domain and path. Also inspect the final URL because a redirect may have moved to another origin.

Call context.cookies() after the server has responded, and pass the correct origin when filtering. The application may store state in local storage or session storage instead of cookies.

A second page is unauthenticated

Create it with context.newPage() from the authenticated context. Do not create a new browser context unless you plan to restore state.

Cookies exist, but requests still return 401

Check expiry, secure transport, same-site restrictions, domain and path, and whether the server binds the token to another property such as a device or CSRF value. A copied cookie cannot recreate server-side state that has expired or been revoked.

Local storage appears to vanish

Verify that both pages use the same origin and context. Install evaluateOnNewDocument() before navigation when the application expects the value before its scripts run. Session storage is tab scoped, so explicitly restore it.

Authentication breaks after a redirect

Record every response and the final URL. A redirect to another host may require a separate cookie, and a secure cookie will not be sent if the flow downgrades to HTTP.

Parallel tests interfere with one another

Give each test its own browser context. Copy a controlled cookie set into a context only when sharing authentication is intentional.

9. Performance, reliability, and cost

  • Reuse contexts when appropriate: Keeping a logged-in context avoids repeated login flows and reduces navigation time.
  • Isolate when correctness matters: Separate contexts prevent cookies and local storage from leaking between tests.
  • Wait for the application’s real readiness signal: Use a specific selector or response when possible; networkidle2 can wait longer on pages with ongoing requests.
  • Keep cookie transfers small: Copy only the origin and cookies required by the destination.
  • Expect server-side expiry: Browser persistence cannot extend a revoked session. Refresh through the application’s supported login or token flow.

Puppeteer itself does not charge per navigation; your costs come from browser runtime, infrastructure, network traffic, and any external service used around the workflow. Preserve state in memory for a single run and use encrypted storage when a session must survive process restarts.

10. Or skip the browser setup

If your goal is a clean screenshot rather than browser automation, ScreenshotNeo provides a single GET request for a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://app.example.com/account -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://app.example.com/account"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://app.example.com/account' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account.

11. FAQ

Use the mechanism the application’s server and client already recognize. A server-issued authentication cookie is usually sent automatically; local or session storage requires application code to read it.

Can I share cookies between unrelated domains?

No. Cookie domain, path, secure, and same-site rules limit where the browser sends them, and the receiving server may reject them.

Does evaluateOnNewDocument() authenticate a user?

No. It seeds page state before scripts run. It cannot create valid server-side authentication without the application’s supported token or cookie.

When should I create a new browser context?

Create one for isolation between users or tests. Reuse the existing context when pages are part of the same authenticated session.

How can I tell whether navigation preserved the session?

Compare context cookies before and after navigation, inspect the final URL, and verify an authenticated-only response or page element.