Browser Session Management for Web Automation: Cookies, Storage, and Profiles
Choose the right browser state strategy for automation: fresh contexts for isolation, saved storage for login reuse, and dedicated profiles for persistence.
Browser session management means deciding where automation state lives, how long it persists, and which task can access it. Use a fresh browser context for isolated tests, save and reload the minimum authentication state needed to reuse a login, and use a dedicated persistent profile when state must survive browser restarts. Treat saved state files and access to a live browser as credentials.
This guide uses Playwright for runnable examples and notes where Puppeteer and Chrome behave differently. The examples use current documented APIs; check the linked framework documentation when implementing against a particular version.
1. Choose the state lifetime you need
| Approach | State lifetime | Isolation | Use it when |
|---|---|---|---|
| Fresh browser context | Until the context closes | Separate from other contexts in the same browser | Each test, account, or simulated user should start clean. |
| Saved storage state | Across test runs, while the saved file is retained | Each context can load a selected state snapshot | Tests need a known authenticated setup without repeating login. |
| Persistent profile directory | Across browser launches | State is tied to that directory | A browser workflow must retain profile behavior on disk. |
| Attach to a live browser | As long as the target browser and session remain active | Automation may see active tabs and profile data | You must inspect or continue a workflow already open in a browser. |
A browser process can contain multiple contexts. A context is the practical boundary for pages and their cookies and other browser state. Playwright describes contexts as incognito-like profiles and says each test can have its own local storage, session storage, cookies, and related state. Puppeteer likewise documents that cookies and local storage are not shared between browser contexts. Playwright: Browser contexts, Playwright: Isolation, Puppeteer: Browser management.
2. Use fresh contexts for independent tests
When a test should not inherit a previous test’s login, cookies, or site preferences, create a new context and close it when done. Reuse the browser process if appropriate, but keep independent users or test cases in separate contexts.
Runnable Playwright example
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext();
try {
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
} finally {
await context.close();
}
} finally {
await browser.close();
}
For multiple users, create one context per user and keep their pages inside the corresponding context. Closing a context releases its pages and temporary state. Do not interpret context isolation as persistence: a new context starts without the prior context’s login unless you explicitly load saved state or use a persistent profile.
Puppeteer equivalent
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
} finally {
await context.close();
}
} finally {
await browser.close();
}
3. Reuse authenticated state with Playwright
For authenticated tests, perform login in an explicit setup step, wait until the application has completed authentication, save the resulting state, then create a fresh context that loads it. Confirm how the application authenticates: cookies, localStorage, IndexedDB, sessionStorage, or another mechanism can be involved.
Save state after login
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext();
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');
// Include IndexedDB when the application stores authentication there.
await context.storageState({ path: 'playwright/.auth/user.json', indexedDB: true });
await context.close();
} finally {
await browser.close();
}
The login URL, accessible labels, post-login URL, and whether IndexedDB is needed depend on the application. Create the output directory before running this example. Store test credentials in environment variables or a secret manager, not in the script.
Load state into an isolated test
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json'
});
try {
const page = await context.newPage();
await page.goto('https://example.com/dashboard');
console.log(await page.title());
} finally {
await context.close();
}
} finally {
await browser.close();
}
Playwright storage state includes cookies and localStorage; its documented option can also include IndexedDB. WebAuthn virtual authenticator state has a separate documented mechanism. Storage state does not automatically cover every application-specific source of identity. See Playwright: Authentication and BrowserContext.storageState.
Keep authentication state private
A storage-state file may contain cookies or headers that let someone impersonate the account. Keep it out of source control, restrict access and retention, and use a test account with limited privileges. Add the auth-state directory to your ignore rules, for example:
# .gitignore
playwright/.auth/
Do not attach the file to bug reports or store it in a broadly accessible artifact bucket. Regenerate it when credentials are rotated, it expires, or the test account changes.
4. Save and restore sessionStorage separately
Do not assume Playwright’s standard storage-state file contains sessionStorage. Playwright documents that it does not persist sessionStorage through that API. If the application relies on it, save the relevant origin’s entries explicitly and restore them before application scripts run. Playwright: Session storage.
Capture sessionStorage after login
const sessionStorageByOrigin = await page.evaluate(() => {
return Object.fromEntries(
Object.entries(sessionStorage)
);
});
// Persist securely alongside the test setup, not in source control.
await writeFile('playwright/.auth/session.json', JSON.stringify({
origin: new URL(page.url()).origin,
values: sessionStorageByOrigin
}));
This snippet assumes a Node.js environment with writeFile imported from node:fs/promises. The saved values can be credential material. Store only the origins and keys the test needs, and protect the file accordingly.
Restore it before the app loads
import { readFile } from 'node:fs/promises';
const saved = JSON.parse(
await readFile('playwright/.auth/session.json', 'utf8')
);
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json'
});
await context.addInitScript(({ targetOrigin, values }) => {
if (location.origin !== targetOrigin) return;
for (const [key, value] of Object.entries(values)) {
sessionStorage.setItem(key, value);
}
}, { targetOrigin: saved.origin, values: saved.values });
const page = await context.newPage();
await page.goto(saved.origin + '/dashboard');
Register the initialization script before navigation so it runs before page scripts read storage. Scope restoration to the intended origin; do not inject one application’s state into unrelated sites. Session storage is associated with an origin and a page session, so validate this approach against the application’s tab, redirect, and popup behavior.
5. Use a persistent profile only when state must survive launches
A persistent profile is a user-data directory on disk. Playwright’s persistent context API launches a browser around that directory and retains browser state across launches. Give automation a dedicated directory separate from your everyday Chrome profile. Playwright documents that multiple browser instances cannot launch using the same user-data directory, so concurrent workers need separate directories. Its current documentation also warns that automating the normal Chrome profile this way can fail due to Chrome policy changes. Playwright: launchPersistentContext.
import { chromium } from 'playwright';
const context = await chromium.launchPersistentContext(
'./automation-profile',
{
headless: true,
viewport: { width: 1280, height: 800 }
}
);
try {
const page = context.pages()[0] ?? await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
} finally {
await context.close();
}
Use a stable, dedicated path with appropriate filesystem permissions. Close the context cleanly so browser data can be flushed. Treat the whole directory as sensitive: it can contain cookies, history, site data, and other profile information. Do not copy it into shared build artifacts or point automation at a personal profile.
6. Attach to an already-running browser carefully
Attaching can be useful when a workflow already has the required state, but it grants automation access to the target browser’s live environment. Playwright’s Chrome DevTools Protocol connection applies to Chromium-based browsers and is documented as lower fidelity than its Playwright protocol connection. Chrome’s auto-connect guidance describes access to tabs, cookies, and browser storage. Confirm the browser, protocol, and scope before connecting; do not treat an everyday signed-in profile as a harmless fixture. Playwright: connectOverCDP, Chrome DevTools: Remote debugging.
import { chromium } from 'playwright';
// Connect only to a browser endpoint you intentionally control.
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
try {
const contexts = browser.contexts();
const context = contexts[0];
if (!context) throw new Error('No browser context is available');
const page = context.pages()[0] ?? await context.newPage();
console.log(await page.url());
} finally {
await browser.close();
}
The remote debugging endpoint must already be enabled and reachable. Keep it bound to a trusted local interface and do not expose it to an untrusted network: a connected client can inspect sensitive browser state. Closing a CDP-connected Playwright browser disconnects the client; it is distinct from closing an ordinary context you created yourself.
7. Choose by isolation, persistence, and exposure
| Question | Fresh context | Saved storage state | Persistent profile | Live attachment |
|---|---|---|---|---|
| Does state survive a run? | No, unless explicitly saved elsewhere | Yes, through the saved snapshot | Yes, on disk | While the target browser remains active |
| Can tests isolate accounts? | Yes, one context per account | Yes, load the matching state per context | Use separate directories per independent profile | Depends on the target browser’s existing contexts and tabs |
| What state is covered? | State created during that context | Cookies and localStorage; optionally IndexedDB; other state may need separate handling | Browser profile data retained by the browser | Whatever the connected browser exposes |
| What is the main exposure? | State held during the run | Credential-bearing file | Entire profile directory | Live access to tabs and browser data |
| Concurrency consideration | Separate contexts are designed for isolation | Separate contexts can load state snapshots | Do not launch multiple instances against one directory | Coordinate access to avoid interfering with a user or another task |
8. cURL, Python, and Node.js screenshot examples
These examples request a page screenshot from ScreenshotNeo. They do not transfer your local browser’s cookies or profile: the API captures the supplied URL in its own browser environment. For authenticated pages, do not assume a screenshot API shares your local session; use the API’s documented authentication or request options if applicable. ScreenshotNeo is a website screenshot API and MCP server by Yorker Media. Its options include custom headers, cookies, and Authorization, along with wait conditions, viewport and device settings, full-page capture, formats, and other controls. See the ScreenshotNeo API documentation.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as output:
output.write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', bytes));
Or skip the browser setup
For a page image or PDF, ScreenshotNeo returns a capture from one GET request, without requiring you to launch or manage a local browser. Cookie banners, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. An MCP server lets Claude, Cursor, and other MCP clients 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. Every feature is on every plan.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the API documentation for request options, and sign up for 1,000 free screenshots a month with no card.
9. Troubleshooting session management
| Symptom | Likely cause | Fix |
|---|---|---|
| A new context is logged out | Context state is isolated and the previous login was never saved. | Run login setup and load its storage state, or choose a dedicated persistent profile if state must survive launches. |
| Storage state loads but the app still redirects to login | The app relies on IndexedDB, sessionStorage, a different origin, or another auth mechanism; the saved credentials may also have expired. | Identify the actual auth store, enable IndexedDB capture when needed, implement explicit sessionStorage restoration, and regenerate expired state. |
| The app reads sessionStorage before the test restores it | Restoration ran after navigation or was not registered as an init script. | Register the origin-scoped init script before creating/navigating the page, and verify redirects stay on the expected origin. |
| Tests intermittently affect each other | Pages share a context, or tests mutate shared server-side account data. | Use one context per test or simulated user. Isolate server-side fixtures as well; browser contexts cannot isolate backend data. |
| Persistent launch reports the profile is in use | Another browser instance is using the same user-data directory. | Stop the other instance or allocate a distinct directory per worker. Do not launch concurrent instances against one profile directory. |
| Persistent launch using normal Chrome data fails | Chrome policy changes can prevent automation from using the everyday profile. | Create a dedicated automation user-data directory instead. |
| CDP connection fails or behaves differently | The endpoint may be unavailable, the browser may not be Chromium-based, or CDP support may differ from Playwright protocol support. | Confirm the browser and endpoint, keep the remote debugging port reachable only to trusted clients, and use Playwright’s normal launch/connect workflow where possible. |
| Saved auth file appears in Git status | The file is not ignored or was saved outside the ignored path. | Add the auth directory to ignore rules, remove any committed credential-bearing file from repository history as appropriate, rotate affected credentials, and restrict artifact access. |
10. Performance, reliability, and cost
Playwright describes browser contexts as fast and cheap to create, so a fresh context is often a practical isolation choice. This is qualitative guidance, not a measured performance claim. Reusing saved authenticated state can avoid repeating interactive login setup, but state expiration and application changes still require refreshes. Persistent profiles avoid rebuilding some browser state but add disk management, cleanup, concurrency, and security responsibilities.
For reliability, make the setup explicit: wait for the application’s authenticated condition, use deterministic test accounts, scope sessionStorage restoration by origin, and close contexts and browsers in cleanup paths. Keep per-test browser isolation separate from backend fixture isolation. Do not rely on a live personal browser as a reproducible fixture.
Browser automation cost depends on the browser infrastructure and how many launches, workers, and runs your environment operates; the cited documentation provides no universal price or benchmark. ScreenshotNeo pricing is 1,000 shots per month free with no card; Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free. Only clean shots are billed, and each response identifies page verdict and billing status in headers. Details: ScreenshotNeo documentation.
11. Frequently asked questions
Does a browser context preserve login after it closes?
No. Save and reload the state you need, or use a persistent profile directory for state that must remain on disk across launches.
Does Playwright storageState include sessionStorage?
No. Playwright documents a separate save-and-restore approach for sessionStorage.
Can two workers share one persistent profile directory?
Do not launch multiple browser instances with the same user-data directory. Allocate a separate directory to each concurrent instance.
Can a screenshot request use the cookies from my local browser?
A remote screenshot request does not automatically inherit a local browser profile. Configure the capture request’s supported authentication options or use a workflow that runs in the browser context holding the session.


