Ephemeral Runners Explained: Why One Job Per Machine Matters
An ephemeral runner executes exactly one job and is then destroyed. That single rule is what makes CI reproducible, and it is also what breaks every habit built on a machine that remembers.
A persistent runner is a long-lived machine that picks up job after job. It is cheap to operate and it accumulates state: leftover containers, a warm package cache, a global tool someone installed by hand two months ago. That accumulated state is why a build passes on one runner and fails on another, and why the fix is so often to reboot the box.
An ephemeral runner registers, runs one job, and terminates. Nothing survives it. Every job starts from the same image, which turns "works on the runner" into a statement about your code rather than about that machine.
Ephemeral versus persistent, in the ways that matter
How just-in-time registration works
A runner that lives for one job cannot hold a long-lived registration token, because a token that outlives the machine is a credential waiting to be reused. Just-in-time registration issues a single-use configuration at the moment a job is queued: the machine boots, claims exactly that job, and its credentials die with it. On Latchkey this takes about ten seconds from cold, or a couple of seconds when a warm machine is already waiting.
What you have to change
- Cache through the cache action rather than relying on a warm local disk, because there is no local disk to be warm.
- Install tools in steps, or bake them into a runner image. A tool someone installed by hand is gone.
- Persist anything you need after the job as an artifact. Local files do not survive.
- Stop debugging by connecting to the machine. Reproduce in a container from the same image instead.
- Expect a per-job startup cost, and reduce the number of jobs rather than the isolation.
What you get in return
The class of failure that starts with "it worked yesterday on the same commit" mostly disappears, because yesterday used a different machine and today does not. Secrets are exposed for the length of one job rather than the lifetime of a host. And capacity stops being a thing you plan: jobs are provisioned when they are queued, so idle machines are not something you pay for or forget to patch.
Applying this to your pipeline
- Measure before changing. Most CI optimisation targets the wrong step because the slow one is assumed rather than timed.
- Cache what is expensive to produce and cheap to validate, and key the cache to the exact tool version.
- Fail fast: run the cheapest checks that can reject a change first, so an expensive job never starts on code that cannot pass.
- Prefer determinism over speed when they conflict. A fast pipeline nobody trusts gets re-run, which is slower than a slow one that is believed.