Runner Infrastructure
Ephemeral Runner
An ephemeral runner accepts one CI job before being deregistered. Secure implementations also dispose of or sanitize the execution environment afterward; GitHub's ephemeral registration flag alone does not delete the host or its disk.
An ephemeral runner accepts one job and then deregisters. The agent lifecycle and the host lifecycle are separate: GitHub's --ephemeral flag does not delete the virtual machine or sanitize its disk.
Definition
The agent is registered for one job. After that job finishes, GitHub deregisters it. The provisioning system must separately dispose of or sanitize the execution environment to prevent unintended state from reaching another job.
Ephemeral runners can use virtual machines, containers, or physical hosts. Isolation depends on the execution boundary and cleanup implementation, not merely the one-job registration rule. Intentionally restored artifacts, caches, or snapshots can carry selected data into a fresh environment.
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
The table below compares disposable per-job machines with long-lived runners: 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 | A fresh execution boundary and cleanup reduce unintended cross-job state; shared caches and credentials still need controls | Jobs can share a filesystem, package cache, and 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 also requires appropriate credentials, network policies, and controls over shared storage. A fresh machine reduces accidental local state reuse, but does not protect a shared cache or external service from credentials that a job can access. Untrusted pull requests should not receive production secrets.
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 jobs normally receive a fresh execution environment. The implementation varies by runner type; single-CPU hosted runners use containers. Consult the runner catalog rather than assuming every hosted job uses its own VM.
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"The unique marker should be absent on a fresh machine. The other checks are illustrative: the base image may already contain TypeScript or container images, so their presence alone does not establish state leakage.
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.
WarpBuild relevance
WarpBuild documents ephemeral VM allocation and destruction for its cloud runners.
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.
Sources
Related FAQ: How do I make a self-hosted runner ephemeral?
Definition reviewed .
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.