Skip to content

The reconciliation loop

Runaway has exactly one path that creates or destroys runner containers: the reconciliation loop. There are no scattered “spawn a runner here” calls elsewhere. Everything that changes Docker state goes through this single door, which is what makes the system predictable.

Each pass compares two things: the pool you asked for (your scale sets and their current demand) against the runners that actually exist. If reality is short, it spawns. If reality has too many idle runners, it reaps. Every action is idempotent — running the same pass twice changes nothing the first pass already settled.

Only one pass runs at a time. Passes are serialized, so two of them can never race to spawn duplicate runners or trip over each other reaping the same container.

  • A 15-second timer — the steady background heartbeat.
  • A container exit — when a runner finishes its one job and dies, that event nudges the loop to spawn its replacement promptly.
  • A change you make — saving a scale set, enabling an org, or adjusting pool size nudges the loop so your change takes effect in about a second rather than waiting for the next tick.

Runaway treats your Docker daemons as the truth and the dashboard as a cache rebuilt from them. The database never overrides reality — reality refills the database.

This is what makes crash recovery clean. When an agent connects (after a hub restart, an agent restart, or a dropped connection), the hub scans that host’s Docker for the runner containers Runaway owns and rebuilds its view from what’s genuinely running. A restart never leaks containers it forgot about and never double-spawns ones it already has — no fragile boot-time bookkeeping to drift out of sync.