Do GitHub Actions Runners Keep State Between Jobs?

No. Each job gets a fresh ephemeral VM whose storage is destroyed when the runner terminates, unless a snapshot runner boots the job from saved VM state.

Last verified:

No. Every GitHub Actions job runs on a fresh runner, and on WarpBuild Cloud runners that runner is a newly allocated ephemeral virtual machine whose storage is deleted the moment the runner terminates, so nothing one job writes to disk is present on the machine the next job receives. The exception is a snapshot runner, which boots a WarpBuild Cloud Ubuntu job from a saved image of an earlier run's VM, so the packages, dependency trees, working directories, and pulled container images that image holds are on disk before the first step executes.

Answer

The unit that resets is the job. Steps inside one job all run on the same machine, so a step can rely on what the step before it installed, wrote, exported, or left running. Crossing a job boundary crosses a machine boundary, and everything machine-local goes with it.

BoundarySame machineDisk state survivesHow to carry state across
Step to step inside one jobYesYesNothing needed
Job to job inside one workflow runNoNoJob outputs, artifacts, cache, or a snapshot alias
Run to run of the same workflowNoNoArtifacts, cache, or a snapshot alias
Snapshot restoreNew machine, saved diskYes, apart from /tmpsnapshot.key=<alias>

This behavior follows from the isolation model rather than from a setting you can flip. All WarpBuild runners run on ephemeral VMs for isolation and security: they are freshly allocated when a job needs them and destroyed when the workflow completes, and runner storage is deleted when the runner terminates. The cloud runners documentation states both properties for every Linux, macOS, and Windows shape in the catalog.

The practical consequence shows up in build times rather than in errors. A five-job workflow on a repository with heavy setup runs the same apt-get install, the same npm ci, the same docker pull, and the same cold compile five times, once per job, because each job starts from an identical base image with an empty local Docker store and no dependency tree.

The one supported way to start a job from a previous job's disk is a snapshot runner. You append snapshot.enabled=true or snapshot.key=<alias> to the runs-on label, capture the machine mid-workflow with the WarpBuilds/snapshot-save action, and later jobs that request the same alias boot from that captured disk. The job stays ephemeral: a fresh VM is still allocated and still destroyed, and what survives is the image. The snapshot runners hub covers the rollout patterns and what are snapshot runners covers the definition and the mechanism in full.

Platform scope matters before you design around any of this. Snapshot behavior applies to the Cloud Ubuntu part of that catalog only.

Detail

The three mechanisms that move state on purpose

Because disks do not survive the job boundary, GitHub Actions gives you three explicit ways to move something across it, and each carries a different payload.

Job outputs carry strings. A step writes to $GITHUB_OUTPUT, the job republishes the value under its outputs block, and a downstream job with a needs edge reads it as needs.<job>.outputs.<key>. The needs edge serializes the two jobs, so an output costs you parallelism.

Artifacts carry files and outlive the run. actions/upload-artifact in the producer and actions/download-artifact in the consumer is the mechanism that guarantees delivery, and it is the right answer whenever the consumer cannot proceed without the payload.

Caches carry directories a later job would otherwise rebuild, on a best-effort basis. A cache entry is scoped to its key, its version, and its branch, and a restore that misses leaves the job to rebuild from scratch. The sharing data between GitHub Actions jobs guide walks through picking between the three by payload size and required lifetime.

All three restore named payloads into a clean machine. That distinction is the whole reason snapshot runners exist. Restoring 3 GB of node_modules still leaves apt packages uninstalled, the local Docker image store empty, the Rust target directory partial, and every file outside the cached paths missing. A snapshot restores the disk, so whatever a previous run left anywhere on it is present at step one.

Which runners honor snapshot labels

Snapshot runners are supported only on WarpBuild Cloud Ubuntu runners. Everywhere else the labels parse and then do nothing. Rates below come from the pricing page and the cloud runners documentation, checked on 2026-08-13.

Runner familyExample labelPer-minute list priceSnapshot labels
Cloud Ubuntu x64warp-ubuntu-latest-x64-8x$0.016Honored
Cloud Ubuntu ARM64warp-ubuntu-latest-arm64-8x$0.012Honored
Cloud macOSwarp-macos-latest-arm64-6x$0.08Silently ignored
Cloud Windowswarp-windows-latest-x64-8x$0.032Silently ignored
BYOC Linux in your cloudBYOC label, $0.002 plus your instance cost$0.002Silently ignored

The word to weigh there is silently. A snapshot label on an unsupported runner type produces no error and no warning in the job log, so a typo in the runner label looks identical to a working configuration. The check that tells you the truth is the WARPBUILD_SNAPSHOT_KEY environment variable, which a runner created from a snapshot carries and a runner booted from the base image does not.

The two labels, and what a snapshot actually holds

Both snapshot labels turn the feature on, and they differ in where the job boots from.

  • snapshot.enabled=true always boots from the base image. It is the label for the job that produces a clean snapshot.
  • snapshot.key=<alias> boots from the saved state for that alias when one exists, and falls back to the base image when none does.

Two behaviors need planning around once state does survive. /tmp does not persist, because the directory is cleaned on reboot and a snapshot boot is a reboot, so any tooling that stages files in /tmp before the save needs a different path. Credentials on disk do persist, because they are part of the disk: an SSH key, an AWS profile, a registry token, or a .npmrc written by a login step all travel inside the image. On public repositories any contributor can read your workflow file and name your alias in a pull request, and on private repositories WarpBuild provisions runners at the organization level, so a snapshot carrying secrets can reach other jobs in the organization. Both cases have the same fix: delete credentials in a step immediately before the save.

Snapshots are also temporary and are deleted after 15 days, so an alias nobody refreshes quietly stops resolving and the next job runs at its cold duration. Full behavior, action inputs, and cleanup guidance live in the snapshot runners documentation.

A workflow where state survives between runs

This shape keeps the image honest. The default branch always boots clean and republishes the alias, and pull requests boot from the published image.

name: build
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: >-
      ${{ github.ref == 'refs/heads/main'
        && 'warp-ubuntu-latest-x64-8x;snapshot.enabled=true'
        || 'warp-ubuntu-latest-x64-8x;snapshot.key=api-main' }}
    steps:
      - uses: actions/checkout@v5

      - name: Report boot source
        run: |
          if [ -z "$WARPBUILD_SNAPSHOT_KEY" ]; then
            echo "booted from the base image"
          else
            echo "booted from snapshot $WARPBUILD_SNAPSHOT_KEY"
          fi

      - name: Install system packages
        if: env.WARPBUILD_SNAPSHOT_KEY == ''
        run: |
          sudo apt-get update
          sudo apt-get install -y libpq-dev protobuf-compiler

      - name: Install dependencies
        run: npm ci

      - name: Build and test
        run: |
          npm run build
          npm test

      - name: Remove credentials
        if: github.ref == 'refs/heads/main'
        run: |
          rm -rf $HOME/.ssh $HOME/.aws $HOME/.npmrc
          git clean -ffdx

      - name: Save snapshot
        if: github.ref == 'refs/heads/main'
        uses: WarpBuilds/snapshot-save@v1
        with:
          alias: "api-main"
          fail-on-error: false
          wait-timeout-minutes: 45

The npm ci step stays unguarded on purpose. It reconciles the lockfile against whatever node_modules tree came out of the image, which is quick when the tree already matches and correct when the lockfile moved on. Guard the steps that are expensive and idempotent, and leave the steps that enforce correctness running every time. fail-on-error: false keeps a failed capture from turning a green pipeline red.

What keeping state costs

Persisting state has two line items on top of runner minutes, both from the pricing page, checked on 2026-08-13: snapshot restore at $0.04 per job and snapshot storage at $0.025 per hour per snapshot. The restore fee is fixed per job, so a snapshot pays for itself only when it removes more than one boot minute plus the fee. On warp-ubuntu-latest-x64-8x at $0.016 per minute, that threshold is 3.5 minutes of skipped setup.

Here is a month with the assumptions stated so you can substitute your own: 1,320 workflow runs (60 per weekday across 22 weekdays), a job that takes 9 minutes on a clean boot of which 5 minutes rebuilds environment state, one alias held live for the full 720 hours, and a snapshot boot that adds a minute. The GitHub column prices the same 9-minute job on the 8-core Linux larger runner at $0.022 per minute, from the GitHub Actions billing reference, checked on 2026-08-13. GitHub-hosted runners have no snapshot equivalent, so that column carries no snapshot line items.

LineGitHub 8-core Linux larger runnerwarp-ubuntu-latest-x64-8xwarp-ubuntu-latest-x64-8x with snapshot
Rate per minute$0.022$0.016$0.016
Minutes per job995
Runner minutes per month11,88011,8806,600
Runner cost$261.36$190.08$105.60
Snapshot restore feesnone$0$52.80
Snapshot storagenone$0$18.00
Total per month$261.36$190.08$176.40

Two readings come out of that table. The same-shape list price does most of the work: warp-ubuntu-latest-x64-8x at 8 vCPU and 32 GB costs $0.016 per minute against $0.022 per minute for the 8-core Linux larger runner at the same shape, which is 27 percent lower list price before any state is reused. Snapshots add $13.68 on these assumptions, and that margin is sensitive: at 3 minutes of setup instead of 5 the snapshot column loses to the plain runner column, exactly as the 3.5-minute threshold predicts. Storage scales with aliases, so ten per-branch aliases held the same way is $180.00 per month.

Do steps in the same GitHub Actions job share state?

Yes. Every step in one job runs on the same machine, so the workspace directory, installed packages, environment files, the local Docker image store, and background processes are all shared between steps. The job is the boundary that resets state; a step boundary resets nothing. Ephemeral runner defines the allocation model that produces this behavior.

How do I carry files from one job to another?

Use the mechanism that matches the payload. Job outputs carry strings through the needs graph, artifacts carry files that outlive the run, and a cache carries directories a later job would otherwise rebuild. All three restore named payloads into a clean machine rather than restoring the machine. The sharing data between GitHub Actions jobs guide compares them, and the persistent caches guide covers cache keys, scopes, and the cache size limit you hit on large dependency trees.

Which WarpBuild runners can boot from saved state?

WarpBuild Cloud Ubuntu runners on x64 and ARM64. BYOC runners, Windows runners, and macOS runners are not supported, and a snapshot label on those runner types is silently ignored, so the job runs normally with no snapshot behavior and no error in the log. The snapshot runners hub lists the Ubuntu labels that accept the feature and their per-minute rates.

How long does saved runner state last?

Snapshots are deleted after 15 days. An alias that no workflow refreshes stops resolving after that window, and the next job requesting it boots from the base image and runs at its cold duration. A workflow that saves on every merge to the default branch never reaches the expiry. The snapshot runners documentation is the reference for the lifetime and the save action inputs.

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.