> For the complete documentation index, see [llms.txt](/llms.txt)

# 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 across browser restarts in an isolated `userDataDir`, the same Chrome mechanism used for user profiles, provisioned per-session by Browserless for isolation across shared fleets.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Persisting State Workflow

You'll need an API token. Grab one from your [Browserless account](https://www.browserless.io/account/home). Create a session via REST with your desired configuration. Save the returned object: it contains the URLs you need for the rest of the session lifecycle.

```javascript

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 below
```

Use the `connect` URL from Step 1 to attach Puppeteer to the running session. The URL already includes your token.

```javascript

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('#toggleIcon button');
await page.screenshot({ path: "after-toggle.jpg" }); // 2. After click
```

When finished, disconnect from the browser and terminate the session using the `stop` URL from Step 1. This permanently deletes the session and all its data.

```javascript

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](/browserql/session-management/persisting-state) 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):**

```javascript

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('#toggleIcon 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.");
}
```

**Python (Playwright):**

```python

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('#toggleIcon 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. The browser restores cookies, localStorage, and cache from disk, but 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.

```javascript
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. The browser still loads persisted data (cookies, localStorage) from disk.

> **Puppeteer only**
> `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 | `false` | Run browser in headless mode. An omitted value resolves to `false`, so a session launches headful unless you pass `true`. Ignored when `stealth: true` (the stealth browser always runs headful). |
| `blockAds` | boolean | `false` | Enable ad blocking |
| `args` | string[] | `[]` | Additional Chrome launch arguments |
| `browser` | string | `'chromium'` | Browser engine: `'chrome'`, `'chromium'`, or `'stealth'`. The `'stealth'` browser cannot be combined with the `stealth` property. |
| `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](/baas/launch-options)) |

For the complete OpenAPI reference, see the [Session API documentation](/open-api/session-create).

## 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.

**Only one client can connect to a session at a time.** If a second client attempts to connect while another is attached, the server returns HTTP 429 with `"Session is already being accessed by another client."` There is no built-in queuing for concurrent session access. Disconnect or close the first client before connecting the next one.

**`timeout` goes on the connection URL, not on session creation.** The `timeout` query parameter controls how long a single WebSocket connection stays open. Append it to the WebSocket URL when you connect. This is separate from `ttl`, which controls the overall session lifetime.

```javascript
// timeout = 2 minutes for this connection
const browser = await puppeteer.connect({
  browserWSEndpoint: `${session.connect}&timeout=120000`,
});
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

<details>
<summary>Can I reopen a session after calling <code>stop</code>?</summary>

No, create a new session instead. The `stop` URL permanently deletes the session's stored browser data (cookies, localStorage, cache), and the session ID stops working, so later requests for it return `404`. Sessions that reach their `ttl` are deleted the same way. To come back to a session later, end your connection and keep the session. `browser.disconnect()` leaves the live browser running for the `processKeepAlive` window, and `browser.close()` ends the browser but keeps the stored data. Only call `stop` when you're done with it.

</details>

<details>
<summary>Can a session close itself when a site blocks it?</summary>

There's no built-in setting for this. Check for the block in your script and stop the session yourself:

```javascript
const response = await page.goto("https://example.com");

// Treat a missing response or a 403 as blocked.
if (!response || response.status() === 403) {
  await browser.close();
  const stopResponse = await fetch(`${session.stop}&force=true`, { method: "DELETE" });
  if (!stopResponse.ok) {
    throw new Error(`Failed to stop session: ${stopResponse.status}`);
  }
}
```

Only call `stop` if you don't need the session's data again, since it's deleted permanently. To react to CAPTCHAs instead of status codes, listen for the [`Browserless.captchaFound`](/baas/bot-detection/captchas) event.

</details>

## Next steps

  <a href="/baas/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot Detection</h3>
      Bypass anti-bot measures with stealth mode and advanced evasion techniques.
    
  </a>

  <a href="/baas/features/human-in-loop-automation" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Hybrid Automation</h3>
      Combine automated scripts with human interaction for complex workflows.
    
  </a>

  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Custom Launch Arguments</h3>
      Configure browser with custom Chrome launch arguments for specialized use cases.
    
  </a>