Skip to content

Streaming

Several methods are server-streaming: the response is a long-lived stream of messages.

Method Stream
LogService.Tail New log lines for a run, job or step, in order
RunService.WatchRun State diffs of a run as jobs and steps change
DeployService.WatchDeployment Deployment progress, strategy steps and verification updates
RunnerService.Connect Bidirectional: runner to server (heartbeats, log chunks, step results), server to runner (job assignments, cancel)
Terminal window
slipway logs run_01H... --follow # LogService.Tail
slipway run watch run_01H... # RunService.WatchRun, with a job/step tree
slipway deploy api 1.4.0 --env staging --wait
for await (const line of logs.tail({ runId })) {
console.log(line.text);
}

The browser uses Connect over fetch streaming. There is no WebSocket and no polling.

Streams can drop (network, deploys of the control plane). Log messages carry a sequence number; reconnect with the last seen sequence to resume without gaps or duplicates. Watch streams begin with a full snapshot, then send diffs, so reconnecting is always safe.

Long-lived streams need proxies configured not to buffer or time out early. For NGINX ingress:

nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-buffering: "off"

Use HTTP/2 end to end where you can. Cloud load balancers often have idle timeouts (for example 60 seconds on some defaults) that you should raise above your longest quiet period. The server also sends periodic keep-alive messages so quiet streams are not cut.

The runner’s control stream is the one bidirectional stream, and proxies that speak HTTP/1.1 to the origin (Cloudflare, classic load balancers) cannot carry it. Runners therefore use a WebSocket at /api/v1/runner/connect by default; your proxy must allow WebSocket upgrades on that path. slipway runner check --server <url> proves the path from a runner host.