ScalebrowserDOCS

Events

The daemon's live stream over SSE or WebSocket: the 23 event types, why every frame is numbered, and how a reader notices it fell behind.

Two transports, the same events, both behind the bearer token. GET /v1/events is Server-Sent Events with a 15 second keep-alive; GET /v1/ws is a WebSocket. Both are server to client only.

bash
$ curl -N http://127.0.0.1:8787/v1/events \
    -H "Authorization: Bearer $SCALEBROWSER_BEARER_TOKEN"

Each connection subscribes before its response is produced, so nothing published between your request and the first frame is lost. A slow client skips events rather than being disconnected.

/v1/ws is this stream and nothing else. It is not a CDP connection. The CDP address comes back from a profile start, and Direct CDP is the page for it.

Every frame is numbered

Each message is one JSON object with a type, an at (Unix seconds) and a seq.

json
{ "type": "profile_started", "seq": 12, "profile_id": "pr_9c21e4",
  "cdp_ws": "ws://127.0.0.1:41337/devtools/browser/2f1c…",
  "headless": false, "at": 1754300112 }

seq counts the frames of this connection, starting at 0, not the frames of the daemon. A global number would only earn its keep with a server-side replay buffer, and there is none.

The number exists so a reader can notice a loss nobody reported: a truncated frame, a parser that dropped one. Compare it with what you saw last.

When you fall behind

The bus is a bounded ring. A reader that cannot keep up has its oldest events overwritten, and that is announced rather than swallowed:

json
{ "type": "resync", "seq": 41, "dropped": 17, "at": 1754300140 }

After a resync, whatever you are showing may disagree with the daemon. Re-read the lists you care about. This is the only event that is injected at the edge rather than published on the bus, because falling behind is a property of one subscriber and not of the system.

The events

There are 23 of them. Every table below is generated from the daemon's own event enum, fields included.

Profile lifecycle

typeFields besides at and seq
profile_state_changedprofile_id, from, to
profile_startedprofile_id, cdp_ws, headless
profile_stoppedprofile_id
profile_crashedprofile_id, reason

Refused starts. Each of these is the stream side of a REST error, so a dashboard learns about a refusal it did not itself ask for: preflight_failed is 4005, capacity_rejected is 4003 (this machine is full), concurrency_rejected is 4008 (the subscription is full).

typeFields besides at and seq
preflight_failedprofile_id, detail
capacity_rejectedprofile_id
concurrency_rejectedprofile_id

Proxies and exits

typeFields besides at and seq
proxy_checkedproxy_id, healthy, country
exit_guard_changedprofile_id, state, addr, class, scope, owned, detail
exit_guard_goneprofile_id

state is ok, degraded, unverified or severed. class is the address kind: mobile, residential, datacenter or unknown. scope says how far the claim reaches: machine while this machine holds no account key, account once it counts everywhere. Proxies & exits explains what each state means for the browser that is running.

Agent activity

typeFields besides at and seq
run_startedrun_id, profile_id, actor, goal
run_steprun_id, profile_id, step
run_finishedrun_id, profile_id, outcome
profile_activity_changedactivity
profile_activity_goneprofile_id

Secrets, recordings, videos and tasks

typeFields besides at and seq
secret_changedprofile_id, secret, count
secret_goneprofile_id, secret_id, label, count
recording_changedrecording
video_changedvideo
video_gonevideo_id
task_changedtask
task_gonetask_id, profile_id

A render's progress rides in video_changed, published every 2 percent or 2 seconds while rendering. A separate job event would be a second truth about the same thing.

Falling behind

typeFields besides at and seq
resyncdropped

Two rules these events follow

Both are worth knowing before you write a reader, because they decide how much bookkeeping you need.

An event carries the whole state of its subject. run_step carries the entire stored step, video_changed the entire video row, task_changed the entire task. Not a selection of changed fields. A display that rebuilds a row from parts is confidently wrong the first time one event goes missing, and you cannot tell from the outside which time that was.

Every list gets arrival, change and departure. That is why exit_guard_gone, secret_gone, video_gone, task_gone and profile_activity_gone exist. Without the departure half, a row that a retention sweep removed at three in the morning stays on an open dashboard until somebody reloads.

secret_changed and secret_gone also carry count, the profile's running total afterwards, so a reader can tell "I missed one" from "there is one".

Writing a reader

Both SDKs wrap the stream, reconnect with backoff and hand you typed events:

python
from scalebrowser import ScalebrowserClient

sb = ScalebrowserClient(base_url="http://127.0.0.1:8787", token="…")

for event in sb.iter_events():
    if event.type == "profile_crashed":
        print(event.profile_id, event.reason)

Handle resync even if you think you will never fall behind: a laptop that sleeps is a slow reader.

Next

  • REST API: the calls that produce most of these events.
  • Errors: the reply side of the refusals above.
  • Activity & runs: what the run events are built into.