Ephemeral runners explained: one job per machine, and what changes
Ephemeral runners execute exactly one GitHub Actions job and are then destroyed, so nothing a job does can be observed by the job after it. That single rule is what makes a build 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: leftover containers, a warm package cache, a registry login somebody did by hand, a global tool installed two months ago for a branch that never merged. That accumulation 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, takes one job, and terminates. Every job starts from the same image, which turns "it works on the runner" from a statement about that machine into a statement about your code. The cost is that everything you were getting for free from a warm machine now has to be asked for explicitly.

What ephemeral actually means
The definition is mechanical rather than philosophical: the runner accepts one job and then unregisters itself. GitHub documents the switch for self-hosted runners as a flag, and the service side of the contract with it: "to add an ephemeral runner to your environment, include the --ephemeral parameter when registering your runner using config.sh", after which "the GitHub Actions service will then automatically de-register the runner after it has processed one job".
Unregistering is not the same as being destroyed, and the distinction matters. A runner that unregisters but keeps its disk has solved the scheduling problem and none of the isolation problem: the next runner process on that host still sees the last job files. Real ephemerality means the machine goes away, which is why the pattern almost always comes with something that creates a fresh instance or container per job.
GitHub-hosted runners already work this way
This is the part that surprises people who think of ephemeral runners as an advanced self-hosting technique. A GitHub-hosted runner is a clean virtual machine provisioned for a single job and discarded afterwards, which is exactly why nothing you install in one job is present in the next, and why the installed software list is a property of the image rather than of the machine.
So if your pipeline runs on ubuntu-latest today, you are already paying the ephemeral costs and getting the ephemeral benefits. Every actions/cache call in your workflow exists because the disk does not survive. Every setup-* action exists because the toolchain has to be chosen per job. The question is not whether to adopt the model but whether a self-hosted fleet you own should keep it.
It is also the reason "works locally, fails in CI" is so often really "works on a machine that has been running for six months, fails on a machine that booted a minute ago". Your laptop is the persistent runner in that comparison.
Just-in-time registration, and why the token shape matters
A machine 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. GitHub offers two shapes. The --ephemeral flag on config.sh registers normally and de-registers after one job. Just-in-time runners go further: the REST API creates the runner registration and returns a configuration that is good for exactly one runner, so the credential is consumed rather than stored.
For a self-hosted fleet, Actions Runner Controller is the usual way to wire this to Kubernetes: a controller watches for queued jobs and creates a pod per job with an ephemeral runner inside it. The characteristic failure mode of that setup is a runner that disappears while GitHub still thinks it has work, which surfaces as the job reporting that the runner lost communication, and it is almost always a pod being evicted or scaled away mid-job rather than a network fault.
On Latchkey the same shape is managed for you. The runners overview describes it as a runner that "registers just-in-time with single-use credentials, runs on a private network with no inbound access, takes exactly one job, and is destroyed", with cold starts around ten seconds.
# Self-hosted, one job then de-register
./config.sh --url https://github.com/ORG/REPO \
--token "$REG_TOKEN" \
--labels self-hosted,linux,x64 \
--ephemeral
./run.shWhat does not survive, and what to do instead
Everything in this list is something a persistent runner was giving you silently. None of it is lost on an ephemeral runner, but all of it has to be requested.
- The package cache. Restore it with
actions/cachekeyed on your lockfile, or use asetup-*action cache input. A cache that is keyed loosely is worse than no cache, because it restores something stale and you debug the wrong thing. - Docker layers. A fresh machine builds from scratch unless you use a registry cache or a build backend that keeps layers outside the runner. This is usually the single biggest ephemeral cost on a Docker-heavy pipeline.
- Registry and cloud logins.
docker loginin one job means nothing in the next. This is a feature: it is what makes OIDC and per-job credentials practical. - Hand-installed tools. Anything somebody installed by hand is gone. Put it in a step, or bake it into an image you control.
- Files you wanted afterwards. Upload them as artifacts in the same job. There is no disk to go back to.
- The ability to log in and look. Debugging becomes reading logs and reproducing in a container from the same image, which is a discipline rather than a loss.
What you get back
The class of failure that starts with "it worked yesterday on the same commit" mostly disappears, because yesterday used a different machine and so does today. That is worth more than it sounds: those failures are expensive precisely because the evidence is on a machine whose state nobody recorded, so the investigation starts from nothing.
Security narrows too. A secret is exposed for the length of one job rather than the lifetime of a host, and there is no shared machine for a compromised job to pivot through, which is the reason GitHub's own guidance says self-hosted runners "should almost never be used for public repositories". GitHub Actions security covers where that sits among the other controls.
And capacity stops being something you plan. Machines are created when jobs are queued, so there is no idle fleet to pay for, patch, or forget to patch. The tradeoff is a per-job startup cost, and the right response to that is fewer, larger jobs rather than less isolation.
Are they slower?
Per job, slightly, and the honest accounting has three parts. There is provisioning time, which is seconds when a warm pool exists and can be minutes when one does not. There is the cold cache, which is real and is the part worth optimizing, because a dependency install from a network cache is much faster than one from the internet and much slower than one from local disk. And there is the time you do not spend debugging failures caused by a dirty machine, which never appears in a benchmark and is usually the larger number.
The mistake to avoid is trading isolation for warmth. Reusing a machine across jobs to keep the cache warm reintroduces exactly the coupling that made builds unreproducible, and the failures it causes arrive weeks later with no obvious link to the change. Keep the machine disposable and make the cache external.
Key takeaways
- Ephemeral means one job then gone: unregistering without destroying the disk only solves half of it.
- GitHub-hosted runners are already ephemeral, so most pipelines are paying these costs already.
- Just-in-time registration exists so a one-job machine never holds a reusable credential.
- Caches, Docker layers, logins and files have to be explicit, because the disk does not come back.
- Do not trade isolation for a warm cache: make the cache external instead.
Frequently asked questions
How do ephemeral GitHub Actions runners work?
--ephemeral flag on config.sh, or a just-in-time registration created through the REST API; in both cases GitHub de-registers the runner once it has processed a single job. A complete implementation also destroys the machine, so no disk state carries into the next job.Why does my ephemeral runner lose communication after the job completes?
Why do ARC runners receive a shutdown signal?
What's the difference between artifacts and cache in GitHub Actions?
Related guides
References
- Self-hosted runners reference: --ephemeral, de-registration and just-in-time runners (read 2026-09-20)
- GitHub Actions security reference: self-hosted runners and public repositories (read 2026-09-20)
- Latchkey documentation: runners overview (read 2026-09-20)
- GitHub-hosted runners reference: labels and specifications (read 2026-09-20)
- GitHub Actions documentation