Ephemeral Runner
An ephemeral runner is a GitHub Actions runner that takes exactly one job and is destroyed afterwards, so no state from that job reaches the next job.
An ephemeral runner is a GitHub Actions runner that accepts exactly one job and is destroyed once that job finishes, so no files, caches, background processes, or credentials from that job carry into the job that runs next. The machine exists for a single unit of work, and its disk is discarded along with it.
The word describes a lifecycle rule rather than a hardware choice. An ephemeral runner can be a virtual machine, a container, or a physical host, on Linux, macOS, or Windows, supplied by you or by a provider, and it stays ephemeral for as long as the one-job rule holds.
Definition
Two properties define the term, and both have to hold.
- One job per runner. The runner agent registers with GitHub, advertises its labels, claims a single job, runs that job's steps, reports the conclusion, then deregisters.
- Disposal after the job. The compute and the storage the job used are thrown away instead of being handed to the job that comes next.
A runner that satisfies only the first property is a weaker form. If the agent deregisters cleanly but the host it ran on stays up and a fresh agent starts in the same working directory, the next job still finds last job's files. The guarantee people want from the word comes from the second property, because that is the one that removes the disk.
How the agent knows to stop after one job
The runner agent is open source and published at github.com/actions/runner under the MIT license. It is the same binary whether the machine belongs to GitHub, to you, or to a provider, and ephemeral behavior is a configuration flag on it:
./config.sh \
--url https://github.com/acme \
--token "$RUNNER_REGISTRATION_TOKEN" \
--labels self-hosted,linux,x64 \
--ephemeral
./run.shThe --ephemeral flag is what changes the lifecycle. Labels are only routing keys, so writing ephemeral into the --labels list is a naming convention that changes nothing about behavior. A runner configured without the flag stays registered and keeps accepting jobs until someone stops it.
The lifecycle, in order
An ephemeral runner moves through the same sequence every time:
- Something creates the machine: a controller reacting to a queued job, an autoscaling group, or a provider's scheduler.
- The agent configures itself with a short-lived registration token and the
--ephemeralflag, then opens an outbound long poll to GitHub and waits. - GitHub sends one job message to a runner whose labels satisfy the job's
runs-onkey. - The agent acknowledges the message, which claims the job so no other runner takes it, and spawns a worker process.
- The worker downloads every action the job references, runs the steps in order, and streams logs back to GitHub.
- The agent reports the conclusion, deregisters itself, and exits.
- The creator of the machine deletes the machine and its disk.
Step 7 is the step teams forget when they build this themselves. An agent that exits without the machine being deleted leaves a paid-for host sitting idle, and worse, leaves a disk that a later agent may reuse.
Ephemeral compared with long-lived runners
A long-lived runner is the other lifecycle: one registration, many jobs, in whatever order the queue hands them over. Neither shape is correct in general, and the tradeoffs are consistent enough to tabulate.
| Dimension | Ephemeral runner | Long-lived runner |
|---|---|---|
| State between jobs | None; every job starts from the base image | Whatever the previous jobs left on disk |
| Isolation | One job per machine, so a compromised job cannot read the next job's checkout | Jobs share a filesystem, a package cache, and often a Docker daemon |
| Cancelled or crashed jobs | Machine is discarded with the mess inside it | Partial files and stray processes stay behind |
| Maintenance | Rebuild the image; the fleet picks it up on the next job | Patch, prune disks, and restart agents on running hosts |
| Reproducing a failure | Same starting state every run | Depends on the host's history |
| Scaling to zero | Natural; no runner exists between jobs | Requires draining a runner mid-lifecycle |
| Start of job work | Setup repeats on every job | Warm caches are already on disk |
State is the property most people mean when they use the term. On an ephemeral runner, a node_modules directory, a Gradle cache, a pulled container image, and a leftover .env file all disappear at the job boundary. Anything a later job needs has to be published deliberately through artifacts, a cache action, or a prebuilt image.
Isolation follows from the same rule. Two jobs that never share a machine cannot read each other's source, secrets exported to files, or credential helpers left configured by a login step. This is why untrusted contributions from forks and jobs handling production credentials are usually pinned to ephemeral fleets.
Maintenance changes shape rather than disappearing. There is no fleet of hosts to patch, prune, or restart, and no disk that fills with Docker layers until jobs start failing on a full volume. In exchange, the image becomes the artifact you own: tooling versions live in an image build pipeline, and rolling out a change means publishing a new image rather than running a configuration tool across live hosts.
Where ephemeral runners show up
GitHub-hosted runners are ephemeral by construction. Each job is placed on a freshly provisioned machine from GitHub's pools and that machine is discarded when the job ends, which is why a hosted job never finds a previous run's files.
Self-hosted fleets choose. Actions Runner Controller, the Kubernetes controller GitHub publishes, watches the job queue and creates one ephemeral runner pod per queued job, which is what makes scale-to-zero safe: there is no risk of terminating a runner in the middle of a job, because a runner only exists while it has one. Runner providers register machines against your organization through the same GitHub registration API, so the lifecycle they implement is described in their own documentation rather than in the workflow file.
Example
The clearest demonstration is a workflow that tries to hand a file from one job to the next without using a documented mechanism.
name: ephemeral-demo
on: workflow_dispatch
jobs:
first:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Leave something behind
run: |
echo "written by job first" > "$HOME/marker.txt"
npm install -g typescript
docker pull alpine:3.20
second:
needs: first
runs-on: ubuntu-latest
steps:
- name: Look for the marker
run: |
if [ -f "$HOME/marker.txt" ]; then
echo "state carried over"
exit 1
fi
echo "clean machine: marker absent"
- name: Look for the globally installed tool
run: command -v tsc || echo "tsc absent"
- name: Look for the pulled image
run: docker image inspect alpine:3.20 || echo "image absent"Job second passes. The marker file is absent, tsc is absent, and the alpine:3.20 image is absent from the local image store, because job second runs on a machine that was created after job first finished and has never seen job first.
Two details in that file are worth reading carefully. The needs: first edge orders the two jobs; it does not connect their filesystems. And job second has no actions/checkout step, so it has no source code either: the repository that job first cloned went away with job first's disk.
Changing the label changes which fleet claims the job while the shape of the workflow stays the same:
jobs:
first:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- run: echo "written by job first" > "$HOME/marker.txt"
second:
needs: first
runs-on: warp-ubuntu-latest-x64-4x
steps:
- run: test ! -f "$HOME/marker.txt" && echo "clean machine"Label shapes like warp-ubuntu-latest-x64-4x encode an operating system, an architecture, and a machine size, and GitHub treats the whole string as one opaque routing key. When the fleet behind that label registers ephemeral runners, the marker check behaves exactly as it does above, because the lifecycle rule lives with the runner rather than with the workflow file. The cloud runners documentation describes the machine shapes behind each label, and the runner security documentation describes the isolation model.
When you wanted the state to survive
A clean machine on every job is the point of the term, and the cost of it is that setup work repeats. Four mechanisms move state across the boundary on purpose:
- Artifacts carry files that a later job needs, uploaded by the producing job and downloaded by the consumer.
- Caches carry directories that a later job would otherwise rebuild, keyed by a lockfile hash so a changed dependency set misses cleanly.
- Prebuilt images move setup out of the job entirely by baking the toolchain into the machine image the runner boots from.
- Saved disk state boots a new machine from an image captured during an earlier run, so the packages and pulled containers from that run are present at step one. Snapshot runners work this way, and the job stays ephemeral because a fresh machine is still allocated and still destroyed.
The time an ephemeral job spends between machine creation and the first useful step is a separate topic, covered under cold start. Boot time, image pull time, and agent registration all land in that window, and it is the number to watch when a fleet moves from long-lived hosts to ephemeral ones.
Related Terms
- Self-hosted runner, defined: who supplies the machine, how registration scope works, and why a self-hosted runner can be either lifecycle.
- Do GitHub Actions runners keep state between jobs?: the same question answered from the workflow author's side, with the mechanisms for moving state across the boundary.
- Snapshot runners and saved disk state: how a job boots from a disk captured on an earlier run while the runner itself stays ephemeral.
- Cold start, defined: the time between machine creation and the first step of a job, and what sits inside it.
- WarpBuild runner security documentation: the compute and storage isolation model for runner virtual machines.
- WarpBuild pricing: per minute rates by runner type.
FAQ
Is an ephemeral runner the same thing as a self-hosted runner?
No. Self-hosted describes who supplies the machine, and ephemeral describes how long the machine lives. A self-hosted runner can be ephemeral or long-lived depending on how the agent was configured, and GitHub-hosted runners are ephemeral by construction because each job gets a fresh machine that is discarded when the job ends.
How do I make a self-hosted runner ephemeral?
Configure the runner agent with the --ephemeral flag, so the agent accepts one job and then deregisters and exits. The second half is up to whatever created the machine: a controller or provisioning script has to delete the virtual machine or container after the agent exits, otherwise the disk survives for the next agent that starts on that host.
Do ephemeral runners lose my dependency cache?
The machine's disk goes away with the machine, so anything warm has to be restored on purpose. The usual mechanisms are actions/cache for dependency directories, artifacts for files a later job needs, a prebuilt image that already contains the toolchain, or a runner that boots from saved disk state captured on an earlier run.
Why does every job repeat the same setup steps on ephemeral runners?
Because every job starts from the same base image with an empty workspace. A workflow of five jobs runs its package install, its checkout, and its container pulls five times unless caching, a prebuilt image, or restored disk state shortens the start of each job.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.