Skip to main content
A runner is a process that hosts an app’s workflows and executes its steps. An app can have many runners. Each one reaches Duraton with connect.

Connect (outbound WebSocket)

Your runner dials Duraton and receives invokes over a persistent WebSocket, so it needs no inbound address and works behind NAT, a firewall, or inside a container with no ingress.
No HTTP server and no register call: connect handshakes, registers the workflow manifest over the socket, answers invokes, and reconnects if the socket drops. There is no heartbeat to configure - Duraton heartbeats the socket itself and keeps the endpoint fresh from that (see liveness). It authenticates with your API key on the socket upgrade. Connect uses the runtime’s global WebSocket. Bun, Deno, and Cloudflare Workers have it built in; on Node it needs Node 22+ (where WebSocket became a stable global) or a polyfill.

Reported metadata

The SDK sends this handshake metadata when it connects; Duraton persists it and exposes it on GET /runners. Every field is self-reported by the runner - only what a runner sends is shown, nothing is inferred.

Liveness

Duraton trusts a runner endpoint only while it keeps checking in: an endpoint Duraton has not seen for 90 seconds ages out of routing, so an anycast run is never sent to a runner that has gone away - a crashed replica, or an endpoint left over from an earlier deploy. Duraton heartbeats the socket every 30s, so a healthy runner is refreshed about three times per window. Nothing to configure. The console’s Apps view shows each runner’s last-seen time and a Live or Stale badge; the same two values are on GET /runners as lastSeenAt and live. A connect runner’s last-seen advances with every socket heartbeat, so one whose process is gone turns Stale instead of sitting on a Live badge indefinitely. A stale runner is listed, not hidden - so you can see that a replica stopped reporting. To take a runner out of routing on purpose, call the connect runner’s close(): it deregisters immediately. See the production guide for the full drain sequence.
An endpoint that is not closed cleanly - either side lost abruptly - stays listed until it ages out, up to the full 90 seconds. An invoke routed to one in that window comes back as a retriable transport error, so Duraton re-dispatches the run to a live runner, or parks it until one appears. The run is not failed, and there is nothing to retry by hand.

Pinning and anycast

Declare a stable runner id and a run can be pinned to that exact instance by setting runner on the event. Omit it on the event and the run is anycast to any of the app’s runners.
See the protocol reference for how a pin is resolved.

When no runner is registered

A run whose app has no live, capable runner does not fail on the spot - and POST /events still returns 202, because dispatch is asynchronous. The run parks and retries: the engine re-checks for a runner about once a second and resumes the moment a capable runner (re)registers. The wait is bounded - after 5 minutes with no runner, measured from a durable stamp so a restart cannot reset it, the run fails terminally with a message naming the workflow and app. This is why the common ordering race self-heals: send an event, then start the runner a moment later, and the queued run simply waits and picks up as soon as the runner is live - you don’t have to register before you emit. The behavior is the same for anycast and pinned runs. A pin to a runner that is briefly absent - a rolling restart of that instance, say - parks and waits for it to come back rather than failing its in-flight runs, up to the same bound. (This is a change from earlier releases, where a pin to a not-currently-registered runner failed fast.) For deploying runners without stranding in-flight runs, see the production guide.