SSE Events
A Server-Sent Events stream carrying the runtime's lifecycle events, so a supervisor, orchestrator or deployment dashboard can react to a runtime's state instead of polling /runtime/status. One long-lived HTTP connection carries every event; the browser's native EventSource handles reconnect on its own, and the same apikey mechanism the rest of /runtime/* uses gates the stream.
These are the same events the WebSocket transport sends. The difference is availability: this stream is served in every runtime state, including Idle with no project loaded, whereas the project API's /api/events/subscribe is only served while playback is running.
GET /runtime/events/subscribe
Requires: read
Opens a long-lived text/event-stream connection. Each event arrives as a pair of lines — an event: line naming the event kind and a data: line carrying a single-line JSON payload — separated by a blank line. The server emits a : keepalive comment every 15 s so dead connections are detected within ~30 s on both ends, and intermediate proxies and NAT mappings stay open on otherwise-quiet streams.
GET parameters:
| Parameter | Required | Description |
|---|---|---|
topics |
No | Comma-separated list of topics to subscribe to. Default: runtime, which carries the lifecycle events below. Other topics on the shared event bus may also be named — log (live log entries), performance (1 Hz metrics while running), project, source, switcher, tally, component — see the HTTP API events reference for those payloads. Unknown topics are accepted silently: they simply never produce events. |
Events on the runtime topic:
Every event name is the topic followed by the event kind, and every payload carries event, state and timestamp plus the extra fields listed here. The kind is identical to the WebSocket event field, so the two transports can be cross-referenced directly.
| Event | Fired when | Extra fields |
|---|---|---|
runtime.snapshot |
Immediately on subscribing | projectName, projectFileName, loaded, runtimeUptimeSeconds, projectUptimeSeconds |
runtime.state_changed |
Any runtime state transition | previousState, reason |
runtime.project_started |
Project start (initial or reload) succeeds | projectName, projectFileName, totalProjectsStarted |
runtime.reload_completed |
Reload completes successfully | projectName, reloadDurationSeconds, reason |
runtime.load_failed |
Load or reload fails | projectFileName, reason |
runtime.project_stopped |
Project stopped, runtime now idle | projectName, reason |
runtime.first_frame |
First frame rendered after start/reload | projectName, timeToFirstFrameSeconds |
runtime.runtime_exit |
Runtime shutting down | reason, uptimeSeconds, totalProjectsStarted, totalReloads, totalErrors, totalWarnings |
runtime.video_worker_crash |
Video worker exited unexpectedly | projectName, severity (critical) |
runtime.snapshot is sent before any live event, and only to subscribers of the runtime topic. The stream carries transitions from the moment you subscribe and has no replay, so the snapshot is what lets a subscriber that connects late — or reconnects — establish current state without a follow-up call.
Ordering: every event that follows the snapshot describes a change that happened after it, so a consumer can apply the snapshot and then apply each event in the order received. The one thing to expect is a repeat: the transition that produced the snapshot's own state may still arrive immediately after it — a snapshot of Running followed by Loading -> Running. Applying that lands on the state the snapshot already reported, so it is safe to apply blindly; a consumer that wants to suppress it can compare against the state it already holds.
runtime.runtime_exit is delivered before the process goes away: shutdown stops each stream and flushes what it is already holding, within a bounded grace period, so a monitoring tool sees why a runtime exited rather than only that its connection dropped. See the WebSocket Events section for the full list of reason values.
Once that event has been sent, the endpoint stops accepting new subscriptions and answers 503 Service Unavailable until the process ends. A subscriber that connected earlier still receives the terminal event; one that arrives afterwards is refused rather than given a stream that could only end without explanation. So a 503 here means the runtime is on its way out — not that the endpoint is unavailable in the state you found it, which is what the same code means on the project API.
Wire format:
event: runtime.snapshot
data: {"event":"snapshot","state":"Running","timestamp":"2026-04-18T12:15:28.000Z","projectName":"MyProject","projectFileName":"C:/Projects/MyProject.prj","loaded":true,"runtimeUptimeSeconds":842.0,"projectUptimeSeconds":830.5}
: keepalive
event: runtime.state_changed
data: {"event":"state_changed","state":"Stopping","timestamp":"2026-04-18T12:15:30.000Z","previousState":"Running","reason":"API reload request"}
event: runtime.reload_completed
data: {"event":"reload_completed","state":"Running","timestamp":"2026-04-18T12:15:32.340Z","projectName":"MyProject","reloadDurationSeconds":2.34,"reason":"API reload request"}
Connection lifecycle: the stream stays open until the client disconnects or the runtime shuts down. A subscriber that goes away is noticed on the next write — within one keepalive interval on a quiet stream — and its subscription is released.
Backpressure and lost events: each subscription is backed by a bounded queue that drops its oldest entries when a client cannot keep up, so one slow subscriber can never delay the runtime. There is no replay buffer and no id: field, so events that fire while a client is disconnected are gone: treat a reconnect as “take the snapshot and resume” rather than expecting to catch up. State is always recoverable from /runtime/status; the stream is best-effort notification, not the source of truth.
Error responses:
| Code | Meaning |
|---|---|
401 Unauthorized |
Missing or invalid API key. |
403 Forbidden |
The key's role lacks read permission. |
503 Service Unavailable |
The runtime is shutting down and is no longer accepting event subscriptions. |
Examples:
# Follow a runtime from the shell
curl -N http://localhost:9101/runtime/events/subscribe?topics=runtime
// Browsers cannot set headers on EventSource, so the key goes in the query string.
const events = new EventSource('http://localhost:9101/runtime/events/subscribe?topics=runtime&apikey=KEY');
events.addEventListener('runtime.snapshot', e => {
const s = JSON.parse(e.data);
console.log(`runtime is ${s.state}, project ${s.projectName || '(none)'}`);
});
events.addEventListener('runtime.state_changed', e => {
const s = JSON.parse(e.data);
console.log(`${s.previousState} -> ${s.state} (${s.reason})`);
});