docs · sessions
Sessions
A session is one Chromium process in one of your workers, with its own empty profile. It counts against your plan’s concurrency from the moment it starts until it ends.
Two ways to get one
Connect and go (WebSocket-born)
Connecting to wss://gw.flatbrowser.com/?token=… creates a session for that connection. When the connection closes, the session ends and the slot is free again — no cleanup call needed. This is what puppeteer.connect() and connectOverCDP() do with the quickstart URL.
To survive a dropped connection, add keepalive=<ms>: the session then stays up that long with no client attached, and you can re-attach (below). Its ID is in the X-Flatbrowser-Session-Id header of the handshake response, and in GET /v1/sessions.
// survive a dropped connection for up to 2 minutes
const browser = await puppeteer.connect({
browserWSEndpoint: 'wss://gw.flatbrowser.com?token=flat_live_YOUR_KEY&keepalive=120000',
});Create, then attach (REST-created)
POST /v1/sessions creates a session that outlives any single connection. Attach to it at /session/<id>?token=… (Browserless’s /devtools/browser/<id> spelling works too), as many times as you like and from more than one client at once. This is the reconnect story: a worker process that restarts simply attaches again.
# 1. create a session that outlives connections
curl -X POST https://gw.flatbrowser.com/v1/sessions \
-H 'Authorization: Bearer flat_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"idleTimeoutMs": 600000, "maxDurationMs": 7200000}'
# → 201 {"id":"3f0c…","connectUrl":"wss://gw.flatbrowser.com/session/3f0c…","wsPath":"/session/3f0c…"}
# 2. attach (and re-attach after a disconnect) — append your key
# wss://gw.flatbrowser.com/session/3f0c…?token=flat_live_YOUR_KEY
# 3. end it explicitly when you are done
curl -X DELETE https://gw.flatbrowser.com/v1/sessions/3f0c… \
-H 'Authorization: Bearer flat_live_YOUR_KEY'How a session ends
| trigger | applies to | default |
|---|---|---|
| the creating connection closes | WebSocket-born sessions without keepalive | immediately |
| no client attached for the idle timeout | REST-created sessions, and WebSocket-born ones with keepalive | 5 minutes (REST); the keepalive value (WebSocket) |
| the maximum duration is reached | every session | 12 hours (also the cap) |
DELETE /v1/sessions/:id | every session | — |
| browser.close() from your client | every session | — |
The idle timer only runs while no client is attached, so a long job that keeps its connection open is never cut off by it — only by the maximum duration.
Timeouts and keepalive parameters
All values are milliseconds. They go on the WebSocket URL or the REST query string; POST /v1/sessions also takes idleTimeoutMs and maxDurationMs in its body. Values above 12 hours are clamped to 12 hours, values below 1000 are refused, and 0 means “use the default”.
| parameter | meaning |
|---|---|
timeout | Total session lifetime — the Browserless meaning. On a REST action (screenshot, PDF, …) it is the guard for the whole action instead: default 60 s, at most 180 s. |
maxDuration | Total session lifetime (native name). Wins over timeout. |
idleTimeout | How long the session may sit with no client attached before it ends. |
keepalive | Same as idleTimeout. On a WebSocket URL it also means the session survives its creating connection. |
queueTimeout | Wait up to this long (max 60 s) for a free slot instead of getting an immediate 429 or 503. |
Both directions of every connection are pinged every 30 seconds, so idle-but-open connections survive load balancers and NATs; your client does not need to send anything to stay connected.
Isolation and state
- Every session starts with a fresh, empty profile in a temporary directory. Cookies, local storage, cache and downloads are deleted when the session ends; nothing carries over to the next one.
- There are no persistent profiles (Browserless’s
profile=anduserDataDirare refused). Keep login state on your side — Playwright’sstorageState, or cookies you export and set again withpage.setCookie()/context.addCookies(). - Workers belong to one account. Sessions of different customers never share a Chromium process or a profile.
- Several tabs and browser contexts per session are fine; they all count as the one session.
Downloads and uploads
- Downloads: fetch the file inside the page and return the bytes, as below; or, in Playwright, read the response body directly (
page.waitForResponse(…)thenresponse.body()). - Uploads with Playwright work:
setInputFiles()sends the file contents to the remote browser (up to 50 MB per call). - Uploads with Puppeteer
uploadFile(path)do not: it sends your local path, which does not exist on the worker. Use Playwright, or build the file in the page with aDataTransfer.
// fetch the file inside the page and hand the bytes back over CDP
const base64 = await page.evaluate(async (url) => {
const res = await fetch(url, { credentials: 'include' });
const buf = new Uint8Array(await res.arrayBuffer());
let bin = '';
for (const b of buf) bin += String.fromCharCode(b);
return btoa(bin);
}, 'https://example.com/report.csv');
const bytes = Buffer.from(base64, 'base64');Restarts and maintenance
When we deploy the gateway, open connections are closed after a short drain period; new connections during the drain get HTTP 503 with Retry-After. REST-created sessions, and WebSocket-born sessions with keepalive, keep running: their connection closes with code 1001 and a reason naming the /session/<id> path to re-attach to. Other WebSocket-born sessions end with their connection. Long jobs should reconnect on close — see errors & limits for the close codes.