For AI agents: a documentation index is available at /llms.txt
Skip to main content

Persisting State

This guide covers creating a long-lived browser session with the Session REST API, running BQL queries against it, and cleaning up when you're done. It also covers session configuration options, how long state persists by plan, and best practices for managing sessions.

Prerequisites

Comparison with BQL reconnect​

The Session API and the BQL reconnect mutation both maintain browser continuity, but they work differently.

FeatureSession API + BQLBQL Reconnect
Session CreationExplicit via REST APIImplicit with first query
Lifecycle ControlProgrammatic start/stopTimeout-based only
State ManagementPersistent across disconnectionsRequires active connection
Proxy ConfigurationSet once, applies to all queriesPer-query configuration

Persisting State Workflow​

  1. Create a Session

    POST to /session. Save the returned object. It contains the URLs you need for BQL queries and session cleanup.

    Stealth is optional

    browserQL is returned for all sessions and accepts queries whether or not stealth is enabled. The examples below pass stealth: true because it helps on bot-protected targets; omit it if you don't need it.

    const response = await fetch(
    "https://production-sfo.browserless.io/session?token=YOUR_API_TOKEN_HERE",
    {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
    ttl: 300000,
    stealth: true,
    }),
    }
    );

    if (!response.ok) {
    throw new Error(`Failed to create session: ${response.status}`);
    }

    const session = await response.json();
    console.log("Session created:", session.id);
  2. Run BQL Queries

    POST BQL mutations to session.browserQL. This is a fully-qualified URL returned for all sessions that inherits all session properties (proxy, stealth mode, etc.).

    const query = `
    mutation SetDarkMode {
    goto(url: "https://docs.browserless.io/", waitUntil: networkIdle) {
    status
    }
    click(selector: "div.toggle_vylO.colorModeToggle_x44X > button") {
    time
    }
    }
    `;

    const bqlResponse = await fetch(session.browserQL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query }),
    });

    const result = await bqlResponse.json();
    console.log("Dark mode toggled:", result.data);
  3. Stop the Session

    You can POST to session.browserQL as many times as needed across multiple script runs. State persists across all of them. Only send a DELETE to session.stop when you want to permanently discard the session data. Sessions also expire automatically when their TTL elapses.

    const stopResponse = await fetch(session.stop, { method: "DELETE" });

    if (stopResponse.ok) {
    console.log("Session stopped.");
    }

Complete Example​

This example creates a session, toggles dark mode in one BQL query, then verifies the theme preference persists in a second query against the same session.

async function main() {
// 1. Create a stealth session
const response = await fetch(
"https://production-sfo.browserless.io/session?token=YOUR_API_TOKEN_HERE",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ttl: 300000, stealth: true }),
}
);

if (!response.ok) {
throw new Error(`Failed to create session: ${response.status}`);
}

const session = await response.json();
console.log("Session created:", session.id);

// 2. First query: navigate and toggle dark mode
const toggleQuery = `
mutation SetDarkMode {
goto(url: "https://docs.browserless.io/", waitUntil: networkIdle) {
status
}
click(selector: "div.toggle_vylO.colorModeToggle_x44X > button") {
time
}
}
`;

const toggleResponse = await fetch(session.browserQL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: toggleQuery }),
});

const toggleResult = await toggleResponse.json();
console.log("Dark mode toggled:", toggleResult.data.click.time);

// 3. Second query: verify theme persists (reuse same session.browserQL URL)
const verifyQuery = `
mutation VerifyTheme {
goto(url: "https://docs.browserless.io/", waitUntil: networkIdle) {
status
}
theme: evaluate(content: "return localStorage.getItem('theme');") {
value
}
}
`;

const verifyResponse = await fetch(session.browserQL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: verifyQuery }),
});

const verifyResult = await verifyResponse.json();
console.log("Theme persisted:", verifyResult.data.theme.value);

// 4. Clean up
await fetch(session.stop, { method: "DELETE" });
console.log("Session stopped.");
}

main().catch(console.error);

Session Response Schema​

A successful POST /session returns a JSON object with these fields:

PropertyTypeDescription
idstringUnique session identifier
connectstringWebSocket URL for CDP-based libraries (Puppeteer, Playwright). Token is pre-embedded.
browserQLstringURL for running BQL queries against this session. Returned for all sessions. Token is pre-embedded.
stopstringURL for session termination via DELETE request. Token is pre-embedded.
ttlnumberSession time-to-live in milliseconds
cloudEndpointIdstring | nullEncrypted cloud endpoint ID for the session. Present on Browserless Cloud, null when self-hosted.

Persisted Session Data Duration​

Session data (cookies, localStorage, cache) persists for the duration of the session TTL, up to the plan maximum:

PlanMaximum Session Lifetime
Free1 day
Prototyping7 days
Starter30 days
Scale90 days
EnterpriseCustom

You can also limit persistence duration by passing a lower ttl value to the session API.

Session Configuration Options​

All session configuration options apply to both WebSocket connections and BQL queries:

ParameterTypeDefaultDescription
ttlnumberrequiredTime-to-live in milliseconds. Max varies by plan.
stealthbooleanfalseEnables stealth mode for bot-protected targets. Optional; BQL queries work without it.
headlessbooleanfalseRun browser in headless mode. An omitted value resolves to false, so a session launches headful unless you pass true. Ignored when stealth is true.
browserstring'chromium'Browser type: 'chromium', 'chrome', or 'stealth'. The 'stealth' browser cannot be combined with the stealth property.
blockAdsbooleanfalseEnable ad blocking.
argsstring[][]Additional Chrome launch arguments.
proxyobjectnullProxy configuration. When set, applies to all BQL queries and WebSocket connections for this session. See proxy docs for the full configuration shape.

Best Practices​

  1. Store session URLs together. Save session.id, session.browserQL, and session.stop as a unit after session creation. You need all three for queries, cleanup, and debugging.

  2. Delete sessions when done. Send a DELETE to session.stop when your workflow completes. Relying on TTL expiry alone wastes resources and can hit session limits.

  3. Handle expired sessions. If a POST to session.browserQL returns a non-2xx status other than 429, the session has likely expired or been deleted; create a new session and retry. A 429 "already being accessed" means the session is alive but still held by the previous request, so retry the same session after a short backoff instead.

FAQ & Troubleshooting​

My session returns a non-2xx status when I try to use it

If the status is anything other than 429, the session has expired or been deleted. Create a new session and retry. Sessions expire based on your plan's TTL (up to 90 days). A 429 means the live session is still attached to another client; see the next entry.

How do I clean up sessions I'm no longer using?

Send a DELETE request to the session.stop URL. Don't rely on TTL expiry alone, as idle sessions consume resources and can hit your session limit.

I get 429 "already being accessed" even though I send requests one at a time

Only one client can be attached to a session at a time, and the attachment isn't released the instant your previous request's HTTP response arrives. The browser detaches shortly after each request completes, so back-to-back requests can catch the session still held by the previous one. Retry with a short backoff (a few hundred milliseconds is usually enough). If a session is done for good, DELETE it via session.stop instead of leaving it to time out.

Next steps​

Was this page helpful?