Skip to content
Latchkey LogoLatchkey home

GitHub Actions cache vs artifacts, and the rule that separates them

GitHub Actions cache vs artifacts is decided by one question: can the job carry on without the file? A cache is an optimization the job must survive losing, and an artifact is an output something later depends on. Everything else follows from that, including two different storage meters, $0.07 a gigabyte-month against $0.25, and two different lifetimes.

Two storage services compared: rate, allowance, measurement, lifetime and who can read each
The two storage services side by side at rates read 21 September 2026. The cache meter reads peak usage per hour above 10 GB per repository; the artifact meter accrues gigabyte-hours against an allowance shared with GitHub Packages.

GitHub's own documentation draws the line in one sentence: cache things that do not change often between runs and are expensive to regenerate, and use artifacts for files you want to keep or view after the run, or to pass between jobs. That is correct and it stops short of the parts that decide real arguments, which are what each one costs, how long each one lives, and which other jobs can see it.

This page puts those side by side. The short version is that they are not two settings on one feature: they are two services with different storage rates, different billing models, different expiry rules and different access scopes, and using the wrong one is usually a bill problem or a security problem rather than a speed problem.

Where they differ, in the places that cost money

Start with the meters, because they are the least-known difference and the largest. Actions cache storage is billed at $0.07 per gigabyte-month past an allowance of 10 gigabytes per repository. Artifact storage is billed at $0.25 per gigabyte-month, pooled with GitHub Packages against a plan-wide allowance that is 500 megabytes on Free, 1 gigabyte on Pro, 2 gigabytes on Team and 50 gigabytes on Enterprise Cloud. That is a factor of three and a half on the rate, and two entirely different allowances.

The billing models differ as well, which matters more than the rates on a spiky pipeline. Cache storage is measured by peak usage in each hour, and only the amount above 10 gigabytes is billable. Artifact storage accrues by the gigabyte-hour all month, and deleting an artifact stops future charges without removing the charges already accrued. A build that uploads 12 gigabytes of artifacts on the first of the month and deletes them on the second has still been billed for two days of them.

CacheArtifacts
Included10 GB per repositoryPlan-wide, shared with GitHub Packages: 500 MB on Free, 2 GB on Team, 50 GB on Enterprise Cloud
Rate above that$0.07 per GB-month$0.25 per GB-month
How usage is measuredPeak usage per hour, above the 10 GB includedGB-hours accrued all month, converted to GB-months
LifetimeRemoved after 7 days without access; oldest evicted first once the repository limit is hitA retention period you set, defaulting to the repository or organization policy, overridable per upload with retention-days
Who can read itThe same branch, the default branch and, on a pull request, the base branchAnyone with access to the workflow run, and other jobs in the same run
If it is missingThe job regenerates the files and carries onThe dependent job fails
Deleting the runUnaffectedAll artifacts for that run are deleted with it
Rate limits200 uploads and 1,500 downloads a minute per repositoryNot published as a per-minute cache-style limit

The rule, and the two ways people break it

A cache must be losable. Anything you put in one has to be something the job can rebuild if the restore misses, because the restore will miss: after seven days without access the entry is gone, and once the repository passes its limit the oldest entries are deleted to make room whether you were finished with them or not.

The first way people break that rule is using a cache to pass a build output to a later job. It appears to work, because within a few minutes of each other the entry is certainly there. Then one Monday the cache is cold, the deploy job restores nothing, and it deploys whatever was in the directory, which is nothing. An artifact would have failed loudly instead.

The second way is the opposite: uploading dependency directories as artifacts to move them between jobs. That works reliably and costs three and a half times as much per gigabyte, accrues by the hour, and eats a plan-wide allowance shared with your package registry. A node_modules directory uploaded on every run of a busy repository is one of the most common ways to be surprised by a storage line.

There is also a security asymmetry that the cost comparison hides. GitHub's own documentation says not to store credentials in a cache path, because anyone who can open a pull request against your repository can read the contents of caches in the base branch. Artifacts are scoped to a run rather than a branch. Neither is a place for secrets, and the cache is the one where that mistake is reachable by a stranger.

What each one prints when it goes wrong

The two failures do not look alike, and that is the useful part. An artifact collision is loud, non-retryable and stops the job; a cache problem is usually a warning that leaves the job green.

The artifact side fails on a name conflict, because an artifact name is unique within a run and uploading the same name twice is an error rather than an overwrite. Matrix jobs that all upload coverage hit this on the second job to finish.

Actions log, upload step, quoted from actions/upload-artifact#769
Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run

The cache side has a comparable conflict and it is not an error. When two jobs race to save the same key, the toolkit turns the reservation failure into a ReserveCacheError, and the calling action logs it rather than failing. The entry one of them wrote is fine; the other job simply did not write one.

Reconstructed from the ReserveCacheError thrown in saveCache in actions/toolkit
Unable to reserve cache with key node-cache-Linux-x64-d5ea0750, another job may be creating this cache.

Choosing, in practice

Dependencies, compiler outputs and downloaded toolchains go in a cache. They are expensive to fetch, cheap to refetch, identical across runs, and nothing downstream breaks if they are missing. Use the setup-* action for your language where one exists, because it picks the directories and writes the key for you.

Build outputs, test reports, coverage files, binaries, screenshots of a failed browser test and logs go in an artifact. Something or someone reads them after the job ends, and a missing one should be a failure rather than a silent empty directory.

The one case that looks like both is a compiled binary you want to test in a later job in the same run. That is an artifact, always, because the later job depends on it existing. If uploading it is slow, reach for retention-days to make it cheap rather than reaching for a cache to make it fast: a one-day retention on a large artifact costs almost nothing under an accrual model, and it keeps the failure loud.

For the caching side in more detail, including what the key is made of and why an entry that exists still does not restore, GitHub Actions caching explained and GitHub Actions cache not restored are the two pages that finish this one.

.github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: npm          # dependencies: losable, so a cache
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v7
        with:
          name: dist-${{ github.sha }}   # output: depended on, so an artifact
          path: dist
          retention-days: 3

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v6
        with:
          name: dist-${{ github.sha }}
      - run: ./deploy.sh

Frequently asked questions

What is the difference between cache and artifacts in GitHub Actions?
A cache stores files that are expensive to regenerate and are the same across runs, such as dependencies, and the job must work when the restore misses. An artifact stores files a job produced that something later reads, such as a binary or a test report, and a missing one is a failure. They are separate services with separate storage meters, allowances, lifetimes and access scopes.
Which is cheaper, GitHub Actions cache or artifact storage?
Cache, by a factor of three and a half on the rate. Cache storage is $0.07 per gigabyte-month past 10 gigabytes included per repository; artifact storage is $0.25 per gigabyte-month against a plan-wide allowance shared with GitHub Packages. Cache is also billed on peak usage per hour, while artifacts accrue gigabyte-hours all month whether anyone downloads them or not.
Can I use a cache to pass files between jobs?
You can, and it will work until it does not. Cache entries are removed after seven days without access and evicted oldest-first once the repository passes its limit, so a job that depends on one will one day restore nothing and carry on with an empty directory. Use an artifact for anything a later job needs, and keep it cheap with a short retention-days rather than moving it into a cache.
How long do GitHub Actions artifacts and caches last?
A cache entry is removed after seven days without being accessed, and may be evicted sooner if the repository is at its storage limit, oldest last-access first. An artifact lives for the retention period set by the repository, organization or enterprise policy, which you can shorten per upload with retention-days. Deleting a workflow run deletes all of its artifacts immediately.
Why does my artifact upload fail with a 409 Conflict?
Because an artifact with that name already exists on the run, and names are unique per run rather than being overwritten. It is most often a matrix where every job uploads the same name. Put the matrix value into the name, or set the overwrite input if replacing the earlier one is really what you want. It is not a storage or size limit.

Related guides

References

Artifacts stay on GitHub wherever the job runs. The cache side is one line on Latchkey. Start free → 30-day trial · No credit card