Persisting State
The Session API lets you create browser sessions via REST — returning a set of URLs and a WebSocket endpoint you use to connect and manage the session lifecycle. Data (cookies, localStorage, cache) persists for days, even after the browser process has been closed and restarted. Under the hood, each session is backed by an isolated userDataDir — the same Chrome mechanism used for user profiles — provisioned and scoped per-session by Browserless to ensure data isolation across connections on shared fleets.
Persisting State Workflow
Create Session
You'll need an API key — grab one from your Browserless account. Create a session via REST with your desired configuration. Save the returned object — it contains the URLs you'll need for the rest of the session lifecycle.
import fetch from 'node-fetch';
const TOKEN = "YOUR_API_TOKEN_HERE";
async function createSession(sessionConfig) {
const response = await fetch(`https://production-sfo.browserless.io/session?token=${TOKEN}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(sessionConfig),
});
if (!response.ok) {
throw new Error(`Failed to create session: ${response.status}`);
}
return response.json();
}
const session = await createSession({
ttl: 180000, // Required: session lifetime in ms (3 minutes here)
});
// Save session.connect and session.stop — you'll need them belowConnect to Session
Use the
connectURL from Step 1 to attach Puppeteer to the running session. The URL already includes your token.import puppeteer from "puppeteer-core";
async function connectToSession(connectUrl) {
const browser = await puppeteer.connect({
browserWSEndpoint: connectUrl,
});
const page = (await browser.pages())[0];
return { browser, page };
}
// connectUrl is session.connect from Step 1
const { browser, page } = await connectToSession(session.connect);
await page.goto("https://docs.browserless.io/");
await page.setViewport({ width: 1000, height: 800 });
await page.screenshot({ path: "before-toggle.jpg" }); // 1. Before click
await page.click('div.toggle_vylO.colorModeToggle_x44X > button');
await page.screenshot({ path: "after-toggle.jpg" }); // 2. After clickClose Session
When finished, disconnect from the browser and terminate the session using the
stopURL from Step 1. This permanently deletes the session and all its data.import fetch from 'node-fetch';
async function stopSession(stopUrl) {
// Append &force=true to terminate immediately even if a client is connected
const response = await fetch(`${stopUrl}&force=true`, { method: 'DELETE' });
if (response.ok) {
console.log('Session stopped.');
return true;
}
const errorText = await response.text();
console.error(`Failed to stop session (${response.status}):`, errorText);
return false;
}
await browser.close();
// stopUrl is session.stop from Step 1
await stopSession(session.stop);
Session Response Shape
A successful POST /session returns a JSON object with these fields:
| Field | Type | Description |
|---|---|---|
id | string | Unique session identifier |
connect | string | WebSocket URL (wss://) for Puppeteer, Playwright, or any CDP-capable library. Token is pre-embedded. |
stop | string | HTTPS URL to issue a DELETE request to terminate and clean up the session. Token is pre-embedded. |
browserQL | string | HTTPS URL to run BrowserQL queries against this session. Token is pre-embedded. See BrowserQL Session Management for more info. |
ttl | number | The TTL value you provided, in milliseconds |
cloudEndpointId | string | null | Cloud routing ID; null on self-hosted enterprise |
Save the connect and stop URLs after session creation — they are your primary handles for the session lifecycle.
Complete Example
- JavaScript (Puppeteer)
- Python (Playwright)
import puppeteer from "puppeteer-core";
import fetch from "node-fetch";
const TOKEN = "YOUR_API_TOKEN_HERE";
async function main() {
const session = await createSession({ ttl: 180000 });
// First connection
const { browser, page } = await connectToSession(session.connect);
await page.goto("https://docs.browserless.io/");
await page.setViewport({ width: 1000, height: 800 });
await page.screenshot({ path: "before-toggle.jpg" }); // 1. Before click
await page.click('div.toggle_vylO.colorModeToggle_x44X > button');
await page.screenshot({ path: "after-toggle.jpg" }); // 2. After click
await browser.close(); // Closes the browser; session data is saved to disk
// Second connection — a new browser process starts on reconnect.
// The browser opens to a blank page; navigate to the site before reading localStorage (it's origin-scoped).
const { browser: browser2, page: page2 } = await connectToSession(session.connect);
await page2.goto("https://docs.browserless.io/");
await page2.setViewport({ width: 1000, height: 800 });
await page2.screenshot({ path: "after-reconnect.jpg" }); // 3. After reconnect — toggle state restored from disk
await browser2.close();
await stopSession(session.stop); // Only stop the session when you want to discard the session data and no longer need to reconnect to it in the future.
}
main().catch(console.error);
// --- Helpers ---
async function createSession(sessionConfig) {
const response = await fetch(
`https://production-sfo.browserless.io/session?token=${TOKEN}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(sessionConfig),
}
);
if (!response.ok) throw new Error(`Failed to create session: ${response.status}`);
return response.json();
}
async function connectToSession(connectUrl) {
const browser = await puppeteer.connect({ browserWSEndpoint: connectUrl });
const page = (await browser.pages())[0];
return { browser, page };
}
async function stopSession(stopUrl) {
const response = await fetch(`${stopUrl}&force=true`, { method: "DELETE" });
if (!response.ok) throw new Error(`Failed to stop session: ${response.status}`);
console.log("Session stopped.");
}
import asyncio
import aiohttp
from playwright.async_api import async_playwright
TOKEN = "YOUR_API_TOKEN_HERE"
async def main():
async with async_playwright() as p:
session = await create_session({
"ttl": 180000,
})
# First connection
browser = await p.chromium.connect_over_cdp(session["connect"])
page = browser.contexts[0].pages[0]
await page.goto("https://docs.browserless.io/")
await page.set_viewport_size({"width": 1000, "height": 800})
await page.screenshot(path="before-toggle.jpg") # 1. Before click
await page.click('div.toggle_vylO.colorModeToggle_x44X > button')
await page.screenshot(path="after-toggle.jpg") # 2. After click
await browser.close()
# Reconnect — a new browser process starts on reconnect.
# Navigate to the site before reading localStorage (it's origin-scoped).
browser2 = await p.chromium.connect_over_cdp(session["connect"])
page2 = browser2.contexts[0].pages[0]
await page2.goto("https://docs.browserless.io/")
await page2.set_viewport_size({"width": 1000, "height": 800})
await asyncio.sleep(1)
await page2.screenshot(path="after-reconnect.jpg") # 3. After reconnect — toggle state restored from disk
await browser2.close()
await stop_session(session["stop"]) # Only stop the session when you want to discard the session data and no longer need to reconnect to it in the future.
# --- Helpers ---
async def create_session(config):
async with aiohttp.ClientSession() as http:
async with http.post(
f"https://production-sfo.browserless.io/session?token={TOKEN}",
headers={"Content-Type": "application/json"},
json=config,
) as r:
if not r.ok:
raise Exception(f"Failed to create session: {r.status}")
return await r.json()
async def stop_session(stop_url):
async with aiohttp.ClientSession() as http:
async with http.delete(f"{stop_url}&force=true") as r:
if not r.ok:
raise Exception(f"Failed to stop session: {r.status}")
print("Session stopped.")
if __name__ == "__main__":
asyncio.run(main())
Keeping Browser Sessions Alive
By default, when all connections to a session close, the underlying browser process is terminated. The next time you connect, a new browser process starts — cookies, localStorage, and cache are restored from disk, but the browser opens to a blank page.
Set processKeepAlive when creating the session to add a grace period. While the timer is running the browser process stays alive, so reconnecting restores the full live state: open pages, scroll position, navigation history, in-memory inputs.
const session = await createSession({
ttl: 180000,
processKeepAlive: 60000, // Browser stays alive for 60s after disconnect
});
After the grace period expires, the next connection starts a fresh browser process — but persisted data (cookies, localStorage) is still restored from disk.
processKeepAlive requires browser.disconnect() to detach without terminating the remote browser. Playwright does not expose a disconnect() method, so this feature is not supported with Playwright.
Persisted Session Data Duration
The data that persists (cache, cookies, localStorage) survives for the duration of the session ttl, up to the plan maximum:
| Plan | Maximum Session Lifetime |
|---|---|
| Free | 1 day |
| Prototyping | 7 days |
| Starter | 30 days |
| Scale | 90 days |
| Enterprise | Custom |
Set ttl to control exactly when session data expires. Sessions are permanently deleted when the ttl elapses.
Session Configuration Options
| Parameter | Type | Default | Description |
|---|---|---|---|
ttl | number | Required | Session lifetime in ms. When reached, the session and all its data are permanently deleted. Must be a non-negative number. Throws 400 if omitted. |
processKeepAlive | number | 0 | Time in ms to keep the browser process alive after the last client disconnects. Within this window, reconnecting restores the full live browser state. After expiry, a new process starts on reconnect but cookies and localStorage are retained. Must be ≤ ttl. |
stealth | boolean | false | Enable stealth mode to avoid bot detection |
headless | boolean | true | Run browser in headless mode. Ignored when stealth: true (stealth always runs headless). |
blockAds | boolean | false | Enable ad blocking |
args | string[] | [] | Additional Chrome launch arguments |
browser | string | 'chromium' | Browser engine: 'chrome' or 'chromium' |
url | string | — | Optional target URL. Some sites require this for specialized handling. |
replay | boolean | false | Enable rrweb session recording for later replay |
proxy | object | null | Proxy configuration (see proxy docs) |
For the complete OpenAPI reference, see the Session API documentation.
Common Pitfalls
ttl is required. Unlike most parameters, ttl has no default. Omitting it returns 400: ttl must be a non-negative number greater than 0.
processKeepAlive must not exceed ttl. The API returns 400 if processKeepAlive > ttl. A safe ratio is processKeepAlive at 10–20% of ttl.
Without processKeepAlive, you must re-navigate on reconnect. When processKeepAlive is 0 (the default), a new browser process starts on every reconnect. The persistent session directory restores cookies and localStorage — but the browser opens to a blank page. Navigate to your target URL before reading localStorage.
Use browser.disconnect() not browser.close() when using processKeepAlive. browser.close() terminates the remote browser immediately, bypassing the keep-alive window. Use browser.disconnect() to detach locally while leaving the browser process running.
stop is permanent. Calling the stop URL permanently deletes the session and all its stored data. Save it, but only call it when you are completely done with the session.
Wrong field names. The session response uses connect (not browserWSEndpoint) and stop (not a manually constructed delete URL). The connect and stop URLs already embed your token.