quickstart →

Magic Cache for GitHub Actions

How to enable RunsOn Magic Cache for actions/cache and language setup actions — unlimited, S3-backed caching with no per-step changes.

The Magic Cache accelerates actions/cache and every language-specific cache action (actions/setup-node, ruby/setup-ruby, …) by swapping their storage backend to a fast, unlimited S3 bucket in your VPC — with no changes to your cache steps.

See how the Magic Cache works for the request path; this page is the practical how-to for GitHub Actions caches.

Availability#

Magic Cache for GitHub Actions works on Linux and Windows runners, with both Flex and Fleet.

Enable Magic Cache#

Keep using actions/cache and language-specific cache actions unchanged.

Flex

The workflow author enables the S3-backed cache sidecar for each job: add extras=s3-cache to the job label and run runs-on/action@v2 ↗ before the cache action.

.github/workflows/ci.yml
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=s3-cache
steps:
- uses: runs-on/action@v2
- uses: actions/cache@v5
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

On an official GitHub-hosted runner, runs-on/action@v2 is a no-op. You can therefore keep the step in a workflow that runs on a mix of GitHub-hosted and RunsOn runners.

Fleet

The platform team enables Magic Cache on the runner definition in Terraform:

main.tf
runners = {
linux-cache = {
cpu = 4
ram = 8
family = ["c8i"]
image = "ubuntu24-full-x64"
extras = ["s3-cache"]
}
}

Workflow authors then target that fleet and keep using their existing cache actions.

Magic Cache also transparently accelerates the type=gha Buildx exporter — see Caching for Docker.

Since v3.5.0, it also serves sccache’s GitHub Actions cache backend (SCCACHE_GHA_ENABLED=true). Earlier versions returned an upload URL that sccache rejects, so sccache never saved anything. In this mode, sccache follows GitHub’s cache scopes like actions/cache: a branch reads its own entries, then the default branch’s. The sccache: s3 input of runs-on/action@v2 instead writes straight to the stack’s S3 bucket, under one prefix that every branch of the repository shares.

Container jobs#

Magic Cache and the artifact proxy work in container: jobs with no extra configuration. The agent writes ACTIONS_CACHE_URL and ACTIONS_RESULTS_URL to $GITHUB_ENV, pointing at the instance’s private IP on port 6871, and the runner forwards them into the job container. Don’t set ACTIONS_* variables in the job’s env:, which would replace those URLs, and don’t run the container with --network none, which can’t reach them.

Accelerate actions/cache#

As an example, the workflow below compares the speed of the Magic Cache vs official runners for multiple cache sizes by generating a random file, saving it to the cache, and then restoring it. It uses the official actions/cache action, but it should also work with any language-specific caching action (e.g. actions/setup-node, ruby/setup-ruby, etc.).

.github/workflows/ci.yml
jobs:
test-magic-cache-speeds:
strategy:
fail-fast: false
matrix:
runner:
- runs-on=${{github.run_id}}/runner=2cpu-linux-x64/extras=s3-cache
- ubuntu-latest
blocks:
- 4096 # 4GB
- 2048 # 2GB
- 512 # 512MB
- 64 # 64MB
runs-on: ${{ matrix.runner }}
env:
FILENAME: random-file
steps:
- uses: runs-on/action@v2
- name: Generate file
run: |
echo "Generating ${{ matrix.blocks }}MiB random file..."
dd if=/dev/urandom of=${{ env.FILENAME }} bs=1M count=${{ matrix.blocks }}
ls -lh ${{ env.FILENAME }}
- name: Save to cache (actions/cache)
uses: actions/cache/save@v4
with:
path: ${{ env.FILENAME }}
key: github-${{github.run_id}}-actions-cache-${{strategy.job-index}}-${{ matrix.blocks }}MiB-${{ env.FILENAME }}
- name: Restore from cache (actions/cache)
uses: actions/cache/restore@v4
with:
path: ${{ env.FILENAME }}
key: github-${{github.run_id}}-actions-cache-${{strategy.job-index}}-${{ matrix.blocks }}MiB-${{ env.FILENAME }}
- name: Restore from cache (actions/cache, restoreKeys)
uses: actions/cache/restore@v4
with:
path: ${{ env.FILENAME }}
key: github-${{github.run_id}}-actions-cache-${{strategy.job-index}}-unknown
restore-keys: |
github-${{github.run_id}}-actions-cache-${{strategy.job-index}}-${{ matrix.blocks }}MiB-

See cache traffic#

The Complete runner step of each job summarizes the job’s Magic Cache traffic, next to the cost estimate:

Magic Cache: 8902 lookups (91% hits), 8184 downloads (3.2 GB, peak 1.1 GB/s), 1533 saves (625.9 MB, peak 180.0 MB/s), 0 errors

Hits include entries restored from a restore-keys prefix. Peaks are the highest transfer rate over one second, which shows whether a restore was limited by the instance’s network. While the job runs, the runner logs the same line every 15 seconds that had traffic, with the p50 and p90 latency of lookups and downloads, in /runs-on/output.log and the instance’s CloudWatch stream. The line is a warning, with the first error, when any request failed.

FAQ#

Speed#

Speed depends on file size and instance type, but the larger the files and the larger the instance type, the faster (up to 5x compared to official runners) it is for saving and restoring.

Size#

The cache has no RunsOn size cap. S3 Lifecycle calculates expiration by adding the fixed retention window configured on your RunsOn stack (default: 10 days) and rounding up to the next midnight UTC. Cache hits do not extend that window.

There is no spending budget that can stop cache saves either. GitHub bills cache storage above the free 10 GB per repository, and once an owner’s budget for it is exceeded, the cache becomes read-only ↗ until billing is resolved or usage drops back under 10 GB. Restores still work, but every save fails with:

Cache reservation failed: You have reached your configured budget, your cache is now read only to prevent additional charges.

Magic Cache saves keep working because its storage is an S3 bucket billed to your AWS account.

Saving a key that already exists#

GitHub’s cache refuses to save a key that already exists for the same branch. Magic Cache replaces the entry instead: the last save wins. actions/cache only saves after a miss on its exact key, so this matters when two jobs miss and save the same key at the same time, or when a workflow calls actions/cache/save unconditionally.

Pricing example#

The following compares 50 GB of cache data in US East (N. Virginia). Magic Cache uses its default 10-day S3 Lifecycle setting, which results in 10–11 days of billable storage because S3 rounds expiration up to the next midnight UTC ↗. It uses Amazon S3 Standard pricing ↗ and GitHub Actions cache-storage pricing ↗. This is a storage-only comparison. It excludes EC2, S3 API and applicable KMS request charges, NAT Gateway, VPC interface endpoints, cross-Region or Internet transfer, taxes, and free-tier credits. It uses 30-day-month proration.

BackendCan retain 50 GB?Storage cost with a 10-day setting
Magic Cache (S3 Standard)Yes$0.38–$0.42
GitHub Actions cache, defaultNo — 10 GB per repository—
GitHub Actions cache, capacity raised to 50 GBYes$0.93

Magic Cache storage costs 50 GB × $0.023/GB-month × 10–11/30 = $0.38–$0.42. GitHub includes 10 GB per repository, so the 50 GB option bills the remaining 40 GB at $0.07/GB-month × 40 GB × 10/30 = $0.93. A GitHub owner must raise the repository’s cache capacity ↗ to retain 50 GB; otherwise, its default cache limit evicts older data.

S3 request and applicable KMS charges still apply. Their total depends on the number and size of cache saves and restores. In the same Region, transfer between S3 and EC2 is free. NAT Gateway, VPC interface endpoints, and cross-Region transfer can add separate network charges.

Magic Cache objects follow that fixed expiry schedule even if they are restored. GitHub removes caches unused for seven days by default ↗, so keep GitHub entries active or configure its retention for a like-for-like comparison.

Scope and isolation#

Magic Cache isolation is opt-in in v3.2. Enable it with EnableCacheIsolation: "true" for Flex CloudFormation or enable_cache_isolation = true for either Terraform module. The cache broker then gives actions/cache and language setup actions temporary credentials limited to the current repository, with GitHub’s restore rules. The scopes come from the job’s GitHub-signed runtime token: a job reads and writes its own ref, and can read the default branch (and, for a pull request, its base branch). So branches and pull requests still fall back to default-branch entries.

Enabling isolation moves Magic Cache data from cache/* to scoped-cache/*. That one-time switch makes the first run for each repository and branch cold; the older objects expire on the normal retention schedule.

Limitations#

  • Magic Cache is opt-in: add extras=s3-cache and runs-on/action@v2 to Flex jobs, or publish cache-enabled Fleet runners.

Common issues#

actions/upload-artifact compatibility#

Since v3.4.0, actions/upload-artifact@v4 and actions/download-artifact@v4 work with the s3-cache extra without any extra step. The runner agent forwards artifact requests to GitHub. The artifact files themselves never go through Magic Cache. This also covers workflows that export ACTIONS_RESULTS_URL through GITHUB_ENV, such as those using crazy-max/ghaction-github-runtime.

On earlier versions, you must also include the runs-on/action@v2 action in jobs that upload or download artifacts. Otherwise you might see an error like:

Attempt 1 of 5 failed with error: Unexpected token 'O', "Original A"... is not valid JSON. Retrying request in 3000 ms...

Alternate: the runs-on/cache action#

The runs-on/cache ↗ action is a more general way to send caches to an S3 bucket — it works with or without RunsOn, including from GitHub-hosted or other providers’ runners. It’s a drop-in replacement for actions/cache that changes only the storage backend (300–500 MB/s throughput, no 10 GB limit, automatic fallback to GitHub’s cache when run outside RunsOn).

See the runs-on/cache repository ↗ for setup (configuring AWS credentials, pointing at your bucket, and the unset-current-credentials option when credentials from a previous step take precedence).