Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Browser automation stays reliable when every step uses the same deliberate session boundary. A session ties your client to a browser or driver; its cookies, storage, open pages and authentication state are what make a later command see the same application that an earlier command changed. Keep that state in the right Playwright BrowserContext, Selenium driver session or durable remote browser, set explicit timeouts, observe events, and clean up deterministically. Reconnect a live remote browser when preserving state and avoiding a cold start matters; relaunch when the browser is unhealthy or isolation is required.
What a browser session actually contains
In Selenium, creating a driver object creates a WebDriver session managed by the driver server. The session ends when you call quit(), which deletes it; close() only closes the current window and can leave the session alive. Selenium’s driver documentation describes this lifecycle at selenium.dev/documentation/webdriver/drivers/.
Playwright separates concerns into a browser process, one or more isolated BrowserContext objects, and pages (tabs) inside each context. Its agent CLI also gives commands a named session. A context is usually the useful state boundary: contexts have their own cookies, cache, local storage, IndexedDB, pages and navigation history. A new context does not share cookies or cache with another one, as documented at playwright.dev/docs/api/class-browser.
Free tools Windows power users keep installed
One-click scans. No signup required.
| Layer | What it controls | Use it for |
|---|---|---|
| Browser process | Chromium, Firefox or WebKit process and its resources | Sharing a process while keeping users isolated in separate contexts |
| Playwright BrowserContext | Cookies, storage, pages, permissions and browser-level settings | One user, tenant, role or test fixture |
| Selenium driver session | Server-managed browser session addressed by a session ID | Sequential WebDriver commands against one browser |
| Page or window | A tab, URL and document | Parallel tabs inside one authenticated context |
| Remote durable browser | Provider-managed process that can survive client disconnects | Long workflows, user handoffs and expensive cold starts |
State scope determines whether a later step can see an earlier login, upload, navigation or consent decision. Do not assume that a new page, driver or context inherits state: choose the boundary explicitly.
#1 Best Overall
How Playwright retains state between steps
One in-memory session
Commands sent to the same Playwright CLI session retain cookies and storage in memory. A persistent mode writes a browser profile to disk, allowing a later process to reopen that profile. This distinction is covered in the Playwright session documentation. In test code, keep the browser open, create one context for the workflow, and create pages within it:
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com/login');
// ...login and subsequent steps use the same context...
await context.close();
await browser.close();
Close contexts before the browser. That gives Playwright time to flush traces, HAR files and videos before the underlying process disappears.
Save authentication with storageState
After a successful login, serialize the context’s reusable state and load it into a fresh context. The API-testing guide at playwright.dev/docs/api-testing and authentication guide at github.com/microsoft/playwright/blob/main/docs/src/auth.md document this pattern:
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →import { chromium } from 'playwright';
const browser = await chromium.launch();
const loginContext = await browser.newContext();
const loginPage = await loginContext.newPage();
await loginPage.goto('https://app.example.com/login');
await loginPage.fill('#email', process.env.TEST_EMAIL);
await loginPage.fill('#password', process.env.TEST_PASSWORD);
await loginPage.click('button[type=submit]');
await loginPage.waitForURL('**/dashboard');
await loginContext.storageState({ path: 'playwright/.auth/user.json' });
await loginContext.close();
const authenticated = await browser.newContext({
storageState: 'playwright/.auth/user.json'
});
const page = await authenticated.newPage();
await page.goto('https://app.example.com/dashboard');
await authenticated.close();
await browser.close();
State files can contain authentication cookies, headers, local storage, IndexedDB data or passkey-related credentials. Playwright warns that such files may enable impersonation; keep them outside source control, restrict file permissions and rotate them like credentials. Session storage is domain-specific and is not covered automatically by the same mechanism, so add your own save and restore code when the application depends on it.
Share API and browser cookies safely
An APIRequestContext associated with a BrowserContext shares that context’s cookies. Responses that set cookies update the context, so an API login can prepare a browser page (or a browser action can update subsequent API calls) without copying headers manually. Keep the request context tied to the intended user context; do not use one mutable context for unrelated accounts.
Rank #2
Resuming authenticated Selenium workflows
Selenium’s session ID and driver keep the browser state while the driver remains alive. A basic workflow sets timeouts, logs in once and performs later actions through the same driver:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = webdriver.ChromeOptions()
driver = webdriver.Chrome(options=options)
driver.set_script_timeout(30)
driver.set_page_load_timeout(300)
driver.implicitly_wait(0)
try:
driver.get("https://app.example.com/login")
driver.find_element(By.ID, "email").send_keys("[email protected]")
driver.find_element(By.ID, "password").send_keys("secret")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
WebDriverWait(driver, 20).until(
EC.url_contains("/dashboard")
)
# Every following command addresses the same authenticated session.
driver.get("https://app.example.com/reports")
finally:
driver.quit()
Selenium documents defaults of 30,000 ms for script execution, 300,000 ms for page loads and 0 for implicit waiting in its driver options documentation. Set them explicitly rather than relying on a grid, language binding or browser-specific default. Prefer explicit waits for a known condition; mixing long implicit waits with explicit waits makes failure timing difficult to predict.
If a process dies, a new driver cannot magically recover the old server session unless your grid or provider exposes a supported attach mechanism. Persisting cookies and other application state can help, but it is not equivalent to preserving every tab, JavaScript object and in-memory page state.
Isolation: one context per identity
Use separate Playwright contexts (or separate Selenium sessions) for different users, tenants, permissions and test cases. Sharing a context creates hidden coupling: one test can change a cookie, local-storage token, selected organization or open tab that another test then observes. Context isolation also makes parallel execution safer. Reuse a context only when the workflow intentionally represents one continuous user.
- Create the context with the minimum permissions and only the cookies or storage it needs.
- Give each parallel worker its own state file or login fixture.
- Close pages and contexts in a
finallyblock so failures do not leak sessions. - Redact session IDs, cookies and authorization headers from logs.
Use WebDriver BiDi for event-driven recovery
Traditional WebDriver is mostly sequential request/response: send a command, wait, inspect the result. WebDriver BiDi adds a WebSocket channel to the W3C model. Selenium describes BiDi support at selenium.dev/documentation/webdriver/bidi/; scripts can subscribe to network requests, console messages, JavaScript exceptions and other browser events.
Rank #3
Events let a runner react to facts as they happen. For example, record a failed API response with its request ID, fail fast on an uncaught page exception, or wait for a specific network event rather than sleeping for an arbitrary number of seconds. Keep event handlers lightweight and correlate them with the test or workflow ID. BiDi improves observability and recovery logic; it does not remove the need for explicit waits or a correctly scoped session.
Recommended Free Tools
When to disconnect and reconnect a remote browser
Reconnect when the browser is healthy
Disconnecting the client while leaving a remote browser alive is useful when a request handler, worker or WebSocket connection must end but the user’s state should remain. Cloudflare’s Browser Run documentation shows browser.disconnect() followed by reconnecting for reusable sessions at developers.cloudflare.com/browser-run/features/reuse-sessions/. Reconnection can avoid a cold launch and preserve cookies, pages and navigation state.
- Use a stable session identifier and authenticate every reconnect.
- Apply an idle timeout and maximum lifetime so abandoned browsers are reclaimed.
- Serialize competing commands; two workers writing the same page at once are not a reliable workflow.
- Store only an opaque handle in a queue or database, never raw cookies or passwords.
Relaunch when the process is unhealthy
Relaunch after a browser crash, lost transport, corrupted profile, unrecoverable page, expired provider lease or a security-boundary change. A reconnect cannot repair a dead process or safely convert one user’s context into another’s. Cloudflare also documents Durable Objects for long-running browsers that must retain state or remain associated with a particular user or route; use that architecture when ownership and routing need to be durable rather than incidental.
Timeouts, cleanup and failure policy
A robust session has a written policy for each operation:
| Operation | Policy to define |
|---|---|
| Navigation | Maximum page-load time and whether redirects or network-idle are required |
| Element action | Explicit visibility, enabled-state and attachment conditions |
| Script execution | Maximum runtime and cancellation behavior |
| Remote lease | Idle timeout, absolute lifetime and reconnect window |
| Shutdown | Close pages, then context, then browser; collect diagnostics before deletion |
On timeout, capture the current URL, page screenshot, console errors, recent network events and a session correlation ID. Retry only idempotent operations, or make writes idempotent with an application key. Replaying a purchase, message or mutation after an uncertain timeout can duplicate the action.
Rank #4
Common session failures and fixes
“Not authenticated” after a new context
Cause: contexts do not share cookies or cache. Fix: load a verified storageState, perform a controlled login in that context, or copy only the required cookie through the framework’s supported API. Check the cookie domain, path, secure flag and expiry.
State disappears after restarting the process
Cause: the profile was in memory. Fix: use Playwright persistent mode or save storageState; for Selenium, persist supported application cookies and relaunch with a clean, known profile. Do not treat a cookie export as a complete copy of sessionStorage or in-memory page state.
“Session ID is null” or disconnected driver
Cause: the driver was quit, the browser crashed, or a grid lease expired. Fix: stop issuing commands, collect logs if available, create a new driver and restore deliberately saved state. Do not call close() when you mean to end the whole Selenium session.
Steps hang or fail inconsistently
Cause: implicit waits, arbitrary sleeps, navigation races or an unobserved JavaScript error. Fix: set explicit timeouts, wait for a semantic condition, subscribe to BiDi or console events where available, and include the failing URL and selector in diagnostics.
Two jobs interfere with one browser
Cause: shared context or concurrent commands. Fix: allocate one context per identity and serialize commands within a context; use a queue or lock around reconnectable remote sessions.
Best Value
Performance and cost decisions
Keeping a browser and context warm reduces launch overhead, but warm state increases memory use and the risk of stale pages or leaked credentials. A practical policy is to reuse a healthy session for a bounded workflow, periodically recycle it, and measure launch time, command latency, memory, timeout rate and reconnect success. In CI, isolation and reproducibility usually outweigh a small launch saving. In an interactive product, a durable remote browser may justify the operational complexity when users expect their tabs and login to remain live.
Or skip the browser setup
If your goal is a clean image or PDF of a page rather than an interactive workflow, ScreenshotNeo provides a single screenshot API and an MCP server for AI agents. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed as clean shots, and response headers identify the page verdict and billing result.
Use the same URL with any supported options; the API base is https://api.screenshotneo.com/v1/shot. Full parameter documentation is at screenshotneo.com/docs/.
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 also supports full-page lazy-image capture, CSS-selector element shots, dark mode, device presets and custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks, selector or network-idle waits, ad and tracker blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, an OpenAPI specification and familiar parameter names for easier migration. Its MCP tools are take_screenshot, get_page_info and capture_pdf.
The Free plan includes 1,000 shots per month without a card. Paid plans start at $5 for 3,000 shots; every feature is on every plan. Sign up for the free plan.
Frequently Asked Questions
Should I store a complete browser profile in source control?
No. Profiles and serialized state can contain credentials or impersonation-capable tokens. Store them in protected secret storage or generate them in a controlled setup step.
Can WebDriver BiDi replace Playwright or Selenium waits?
No. BiDi supplies event streams; you still need explicit conditions, timeouts and cleanup for commands that act on the page.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Is reconnecting always cheaper than launching a browser?
No. Reconnection avoids a cold start only when the remote browser is healthy and the provider keeps it alive. Leases, idle limits, memory use and recovery complexity can make a fresh isolated session preferable.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

