Lifecycle
How long a session lives, why it stops, how long a stopped session is kept, and how to find, update, stop, resume and delete sessions, one at a time or in bulk.
A session starts RUNNING. When its work is done, you stop it: the platform saves it exactly as it is and frees the machine, and billing stops. A stopped session can be resumed later, with the same id. Or you delete it, which ends it for good. This page covers the whole life of a session, and how to manage many at once.
Statuses
| Status | What it means | Billed |
|---|---|---|
RUNNING | The machine is up. You can connect, run commands and read files. | Per second |
STOPPED | Saved, with no machine. Resume it to use it again. It is kept until its deletesAt (your plan’s retention days), then deleted. It does not count towards your concurrent sessions. See Stop and resume. | No |
DELETED | Ended for good. What it saved, its recording, its logs and its pages are gone. The session record stays, with its usage and cost, for billing. | No |
ERROR | The session never started. error says what failed. | No |
Stopping is how a session’s work always ends: you stop it, or the platform does when its time runs out, it goes idle, or a limit is reached. There is no other way. A session is deleted only when you delete it, or when its retention days pass.
How long a session lives
- Every session stops when it reaches its
timeout(stopReason: "timeout"), unless you stop it first.expiresAttells you when. Resuming it starts a new clock oftimeoutseconds. To give a running session more time, change itstimeout(see Update a session). - A browser-only session without
keepAlivestops about 5 seconds after its last CDP client disconnects ("no_clients"). The short grace period lets a client reconnect. SetkeepAlive: trueto keep it running between connections. It never stops this way while an agent run, a script or a plain-English step works in it, or while a run in it can still be resumed. - A session with a shell, and one no client ever connected to, keeps running until you stop it or it times out.
- With an
idleTimeout(opt-in, 30 seconds up to its timeout), it stops after that long with no activity at all ("idle"), up to about 15 seconds late. Use it forkeepAliveand shell sessions whose client may crash. See idleTimeout for crashed clients.
Every rule for how sessions and agent runs end, with best practices, is on How runs and sessions end.
Why a session stopped
A stopped session shows stoppedAt and stopReason. Both are null while it runs, and a resume clears them.
stopReason | What happened |
|---|---|
requested | You (or the console) stopped it. |
timeout | It reached its timeout. |
idle | Its idleTimeout passed with no activity. |
no_clients | A browser-only session without keepAlive lost its last CDP client, with no agent run, script or step working in it. |
agent_run | An agent run finished and stopped the session it had created, or its resume window passed unused. |
task | A task’s run finished and stopped its session. |
machine_lost | Its machine failed and could not be recovered. What it saved is its last checkpoint; error says what happened. With no checkpoint, nothing was saved and a resume answers 409 nothing_saved. |
api_restart | The API restarted and the session’s machine could not be taken back, with no checkpoint to start it again from. Nothing was saved. |
account_recovered | A password reset recovered an account whose email was not confirmed. Nothing is kept. See Password resets. |
suspended | An admin suspended the organization. Its running sessions stop, saving what they can. See Suspended organizations and projects. |
spend_limit | The organization or the project reached the monthly spending limit its owners set. Running sessions stop within about a minute, saved like any stop. See Credit and spending limits. |
out_of_credit | The organization had no credit left. Used only when refusing work at zero credit is switched on, which it is not yet: today, running out of credit only sends an alert. |
How long a stopped session is kept
A stopped session keeps its saved browser and files, its recording, its logs and its pages, and the steps of agent runs in it, for your plan’s retentionDays, counted from stoppedAt.
| Plan | Days a stopped session is kept |
|---|---|
| Free | 7 |
| Hobby | 30 |
| Startup | 30 |
| Scale | 90 |
- The day is fixed when the session stops:
deletesAt = stoppedAt + retentionDays. A later plan change does not change it. If you resume the session and stop it again, it gets a newdeletesAt. - When
deletesAtpasses, the session is deleted:status: "DELETED",deleteReason: "retention", and asession.deletedwebhook. Resume a session before then if you need it. - The session record stays after a delete, with its usage and cost. Agent runs keep their task, result and token counts. Browser profiles, usage and billing records are not affected.
- While an organization is suspended, nothing of it is deleted. The days keep counting, so what has passed is deleted once the suspension is lifted.
Checkpoints and recovery
The platform saves every running session’s whole browser and its workspace about every 5 minutes. This is a checkpoint; checkpointAt shows the last one. The browser is saved while it runs. Tabs with their history, cookies, every site’s storage and form values are in it; the caches are not. See What a session keeps.
If a machine stops (a crash, or a lost server), the platform starts the session again on a fresh machine from its last checkpoint. It keeps its id and its URLs. recoveries counts how often this happened, and a lifecycle event says how old the checkpoint was. Work done after that checkpoint is lost, and running processes start again. A recovery can miss the last few seconds of changes in the browser, because a checkpoint copies it while it runs.
If the browser is too big to save whole (over 256 MB compressed for a checkpoint, over 1 GB compressed for a stop), or the machine is from an older version, the session comes back with what its open tabs held, and a lifecycle warning says so, once per session and reason. See When the browser cannot be saved whole.
When a machine is about to go away (a planned server change), the platform starts the session on a new machine first, with nothing lost. You do not need to do anything.
When the platform’s API restarts, for example during a deploy, sessions keep running: the new API takes their machines back. An agent run working in one ends as server_restarted, and can be resumed in the same browser. See When the API restarts.
Find sessions
GET /v1/sessions lists your sessions, newest first, with filters. The response has total (how many match) and next (a cursor for the next page). See Pagination.
| Query | Meaning |
|---|---|
status | One or more of RUNNING, STOPPED, DELETED, ERROR, comma-separated. |
kind | browser (no shell), combined (browser and shell) or shell (no browser). See Three kinds of session. |
q | The start of a session id, or text inside userMetadata. |
from, to | ISO dates, compared with createdAt. |
sort | created_desc (default), created_asc or duration_desc. |
limit, after | Page size (default 50, at most 500) and the next value of the previous page. Pages are always read with after. |
curl "https://api.boxline.dev/v1/sessions?status=RUNNING,STOPPED&q=customer-42&limit=25" \ -H "x-api-key: $BOXLINE_API_KEY"# Only sessions with a shell and no browser:curl "https://api.boxline.dev/v1/sessions?kind=shell&status=RUNNING" \ -H "x-api-key: $BOXLINE_API_KEY"# The next page: send the previous answer's "next" back as "after":curl "https://api.boxline.dev/v1/sessions?limit=25&after=$NEXT" \ -H "x-api-key: $BOXLINE_API_KEY"const bx = new Boxline();// One page, with the total, for tables:const page = await bx.sessions.list({ status: ["RUNNING", "STOPPED"], q: "customer-42", limit: 25 });console.log(page.total, page.data.map((s) => s.id), page.next);// Every match, page after page:for await (const s of bx.sessions.list({ status: "RUNNING" })) console.log(s.id, s.status);// Only one kind of session: "browser", "shell" or "combined":const shells = await bx.sessions.list({ kind: "shell", status: "RUNNING" });bx = Boxline()# One page, with the total, for tables:page = bx.sessions.list(status=["RUNNING", "STOPPED"], q="customer-42", limit=25)print(page.total, [s.id for s in page.data], page.next)# Every match, page after page:for s in bx.sessions.list(status="RUNNING"): print(s.id, s.status)# Only one kind of session: "browser", "shell" or "combined":shells = bx.sessions.list(kind="shell", status="RUNNING")boxline sessions list --status running,stopped --limit 25boxline sessions list --status running --all # every page# The CLI has no q or kind filter: use the API for those.In the SDKs, a list call returns its first page, and iterating it goes through every match. See Pagination.
Get a session
GET /v1/sessions/:id returns the session object, with connectUrl, liveUrl and terminalUrl signed afresh every time. A stopped or deleted session has no URLs, and each URL is null for a part the session was created without (see Three kinds of session).
curl https://api.boxline.dev/v1/sessions/$SESSION_ID \ -H "x-api-key: $BOXLINE_API_KEY"const session = await bx.sessions.get(sessionId);await session.refresh(); // later: reload its data, with fresh URLsconsole.log(session.status, session.data.expiresAt);// A stopped session also shows stoppedAt, stopReason and deletesAt.session = bx.sessions.get(session_id)session.refresh() # later: reload its data, with fresh URLsprint(session.status, session.expires_at)boxline sessions get $SESSION_IDUpdate a session
PATCH /v1/sessions/:id is the one call for every change to a running session. It takes the same field names and shapes as Create a session. Send any of these together; fields you leave out do not change. The checks run first and the changes after, so a bad value leaves the session as it was. The answer is the session.
| Field | What it does |
|---|---|
timeout | The session’s length in seconds, counted from its last start (a resume starts it again). expiresAt follows. At least 60 seconds from now, at most your plan’s longest session (402 plan_limit above it). See Give a session more time. |
idleTimeout | Seconds without activity before it stops, counted from now, 30 up to its timeout. null switches it off. |
keepAlive, userMetadata | As on create. |
proxy | A new proxy, applied at once: open connections are closed, so every request after the change uses it. null removes the proxy. See Proxies. |
rotateProxy | true gives the proxy a new IP now (a sticky IP stays the same until then). 400 without a proxy. See Proxies. |
rotateUrls | true makes every earlier connectUrl, liveUrl and terminalUrl stop working. The answer carries the new ones. See Live view. |
captcha | "ask", "ignore" or "solve", applied at once. See CAPTCHAs. |
browser | The browser’s settings, { mode, locale, timezone }, never on or off. See Browser mode. |
blockAds, cookieBanners | Applied at once. See Ad and cookie-banner blocking. |
A change that needs a feature your plan does not include, such as a residential proxy or captcha: "solve", gets 402 feature_not_in_plan (see Plans and features). A session that is not running answers 409 session_not_running.
Fixed when the session is created
These cannot change while the session lives. Sending one answers 400 not_updatable and names the field; the SDKs raise it as a NotUpdatableError.
browserorshellon or off (the kind of session; only the browser’s settings above change).viewport,profileandrecordSession.setup,extensions,envandcredentials.
For any of these, create a new session with the setting you need.
curl -X PATCH https://api.boxline.dev/v1/sessions/$SESSION_ID \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d '{"keepAlive": true, "userMetadata": {"customer": "customer-42"}}'# A viewport cannot change: 400 not_updatable, naming "viewport"curl -X PATCH https://api.boxline.dev/v1/sessions/$SESSION_ID \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d '{"viewport": {"width": 390, "height": 844}}'await session.update({ keepAlive: true, userMetadata: { customer: "customer-42" } });await session.update({ captcha: "ignore" }); // agent runs and steps no longer wait for CAPTCHAsawait session.update({ browser: { mode: "realistic" } }); // clock and language follow the proxyawait session.update({ proxy: { type: "datacenter", country: "DE" } }); // a new proxy, at onceawait session.update({ proxy: null }); // no proxyawait session.update({ blockAds: true, cookieBanners: "off" });session.update(keep_alive=True, user_metadata={"customer": "customer-42"})session.update(captcha="ignore") # agent runs and steps no longer wait for CAPTCHAssession.update(browser={"mode": "realistic"}) # clock and language follow the proxysession.update(proxy={"type": "datacenter", "country": "DE"}) # a new proxy, at oncesession.update(proxy=None) # no proxysession.update(block_ads=True, cookie_banners="off")Give a session more time
A session’s timeout is its whole length, counted from its last start, so to add time, send a bigger number: the current timeout plus what you need. expiresAt follows. The total can never pass your plan’s longest session; past that you get 402 plan_limit. A link is valid until 5 minutes after the expiresAt it was signed with, so take the fresh links from the answer. The session.expiring webhook comes about a minute before a session stops by time, a good moment to raise its timeout; an agent run shows its session’s end as sessionExpiresAt.
curl -X PATCH https://api.boxline.dev/v1/sessions/$SESSION_ID \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d '{"timeout": 1500}' # it had 900: ten more minutesconst session = await bx.sessions.get(sessionId);await session.update({ timeout: session.data.timeout + 600 }); // ten more minutesconsole.log(session.data.expiresAt); // ten minutes later than beforesession = bx.sessions.get(session_id)session.update(timeout=session.timeout + 600) # ten more minutesprint(session.expires_at) # ten minutes later than beforeStop a session
POST /v1/sessions/:id/stop saves the session exactly as it is (the whole browser, with every tab and its history, and the files in /workspace), frees the machine and returns the session with status STOPPED. Billing stops at that second. Stopping a session that is already stopped is safe: you get it back as it is. If the state cannot be saved, the call fails and the session keeps running: stop it again.
If the session uses a browser profile with persist: true, its cookies and storage are saved to the profile too.
curl -X POST https://api.boxline.dev/v1/sessions/$SESSION_ID/stop \ -H "x-api-key: $BOXLINE_API_KEY"const session = await bx.sessions.create();try { // … work with the session …} finally { await session.stop(); // also when something failed}console.log(session.status, session.data.deletesAt); // "STOPPED", the day it will be deleted# Leaving the "with" block stops the session, also after an error.with bx.sessions.create() as session: ...boxline sessions stop $SESSION_IDA stop ends the session’s signed URLs for good. It rotates connectUrl, liveUrl and terminalUrl, as rotateUrls: true in Update a session does: the links you held stay refused after a resume, and the session that resume returns has fresh ones. So a link you shared cannot reach a session that was stopped and resumed. Share the fresh links only with the people who should have them.
Resume a session
POST /v1/sessions/:id/resume brings a stopped session back on a fresh machine, with the same id and everything as it was when it stopped, and returns it with status RUNNING. It gets a full timeout again, and fresh connectUrl, liveUrl and terminalUrl. You can stop and resume a session any number of times. Running processes and page memory do not come back: pages reload. See What a session keeps.
A call on a stopped session does not resume it. Browser actions, commands, writing files, the live view and connect answer 409 session_not_running until you call resume. Reading its files works without a resume: see Files of a stopped session. An agent run given a stopped sessionId resumes it itself.
| Answer | Why |
|---|---|
409 nothing_saved | Nothing was saved when the session stopped: its machine was lost before its first checkpoint, or it stopped without saving (see Why a session stopped). Start a new session. |
409 session_not_stopped | The session was deleted. A deleted session cannot come back. |
409 conflict | Its browser profile is in use by another running session. |
429 concurrency_limit | Your plan’s concurrent sessions are all running. Stop one first. |
402 spend_limit, 402 out_of_credit | A spending limit is reached, or there is no credit. Raising the limit lets work start again. |
curl -X POST https://api.boxline.dev/v1/sessions/$SESSION_ID/resume \ -H "x-api-key: $BOXLINE_API_KEY"import { NothingSavedError } from "@boxline/sdk";const session = await bx.sessions.get(sessionId); // status "STOPPED"try { await session.resume(); // RUNNING again, same id, fresh URLs} catch (err) { if (err instanceof NothingSavedError) console.log("nothing was saved: start a new session"); else throw err;}from boxline import NothingSavedErrorsession = bx.sessions.get(session_id) # status "STOPPED"try: session.resume() # RUNNING again, same id, fresh URLsexcept NothingSavedError: print("nothing was saved: start a new session")boxline sessions resume $SESSION_IDDelete a session
DELETE /v1/sessions/:id ends the session for good and returns it with status DELETED. A running session is stopped without saving first. What it saved (the browser and the files), its recording, its logs, its pages and the steps of agent runs in it are deleted now. The session record, with its usage and cost, stays for billing. A delete cannot be undone. Deleting a session that is already deleted is safe: you get it back as it is.
curl -X DELETE https://api.boxline.dev/v1/sessions/$SESSION_ID \ -H "x-api-key: $BOXLINE_API_KEY"await session.delete(); // or bx.sessions.delete(sessionId)console.log(session.status, session.data.deleteReason); // "DELETED", "requested"session.delete() # or bx.sessions.delete(session_id)print(session.status, session.delete_reason) # "DELETED", "requested"boxline sessions delete $SESSION_IDAct on many sessions
POST /v1/sessions/bulk with { action, ids } stops, resumes or deletes up to 100 sessions. action is "stop", "resume" or "delete". Each id gets its own result, with the refusals of a single call (for a resume, nothing_saved and the others), so one failure never blocks the rest.
curl -X POST https://api.boxline.dev/v1/sessions/bulk \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d '{"action": "stop", "ids": ["<session-id>", "<session-id>"]}'// Up to 100 ids per call: one page of 100.const running = await bx.sessions.list({ status: "RUNNING", q: "nightly-crawl", limit: 100 });const { results } = await bx.sessions.bulk("stop", running.data.map((s) => s.id));console.log(results.filter((r) => !r.ok));# Up to 100 ids per call: one page of 100.running = bx.sessions.list(status="RUNNING", q="nightly-crawl", limit=100)result = bx.sessions.bulk("stop", [s.id for s in running.data])print([r for r in result["results"] if not r["ok"]])These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.