# Cache Go modules GitHub Actions runs, and the file the key changed to

> Cache Go modules GitHub Actions jobs with setup-go: it is on by default, it keys on go.mod since v6.3.0, and it has no restore-keys.

Source: https://latchkey.dev/learn/speed/cache-go-modules-in-github-actions  
Updated: 2026-09-21

To cache Go modules GitHub Actions jobs already does it for you: `actions/setup-go` has `cache` defaulting to true, and it restores both the module cache and the build cache in one entry. Two details decide whether that entry ever hits: since v6.3.0 the key hashes `go.mod` rather than `go.sum`, and there are no restore keys at all, so a miss is total.

Most Go caching advice on the internet tells you to add `cache: true` to `actions/setup-go`. That input has defaulted to true since v5, so the advice is a no-op and the interesting questions are the ones underneath it: which file the key hashes, which directories get stored, when the entry is written, and what happens on a near miss.

All four answers live in about sixty lines of the action, and one of them changed recently enough that a workflow copied from a blog post in 2025 now behaves differently from the same workflow today. This page walks the key as the action builds it, names the version where the file it hashes changed, and shows the warning you get when it cannot build a key at all.

## What the action caches, and what the key is made of

The action asks Go where its directories are rather than hard-coding paths: it runs `go env GOMODCACHE` and `go env GOCACHE` and caches both. That pairing is the whole reason the action is worth using over a hand-rolled `actions/cache` step on `~/go/pkg/mod`. The module cache saves the download and the build cache saves the compile, and on a project of any size the compile is the larger number.

The key is then assembled from five pieces in a fixed order: a `setup-go` prefix, the runner OS, the Node process architecture, the image identifier on Linux, the Go version spec you asked for, and a hash of one dependency file. Nothing about your repository beyond that file enters the key, which is why two branches with the same dependencies share an entry and why bumping a patch Go version invalidates everything.

| Piece of the key | Where it comes from | What changes it |
| --- | --- | --- |
| `setup-go-` | A literal prefix in the action | Nothing |
| The platform | `RUNNER_OS` | Moving a job from Linux to macOS or Windows |
| The architecture | Node's `process.arch` on the runner | Moving between x64 and arm64 runners |
| The image, on Linux only | The `ImageOS` variable the runner sets | A monthly runner image roll, which is out of your hands |
| The Go version spec | The `go-version` you asked for, parsed | Any change to the requested version, including a patch |
| The dependency hash | `hashFiles` over one file: `go.mod` from v6.3.0, `go.sum` before that | Editing that file, and on older versions only that file |

> Read from `src/cache-restore.ts` and `src/package-managers.ts` in `actions/setup-go` on 21 September 2026. The `ImageOS` component is the one people are surprised by: when GitHub rolls a new Ubuntu image your Go cache goes cold on every repository at once, and nothing in your workflow changed.

## The file it hashes changed in v6.3.0

Until v6.2.0 the action hashed `go.sum`. From v6.3.0 onward, and in v7, it hashes `go.mod`. The change is a single constant in `src/package-managers.ts`, and the release notes for v6.3.0 name it as "Update default Go module caching to use go.mod".

That is a smaller change than it sounds for most repositories and a real one for two kinds. A project with no third-party dependencies has no `go.sum` at all, and on the old default the action could not build a key and gave up; on the new default it works. A project that pins by `go.sum` and edits it independently of `go.mod`, which happens when only a transitive checksum moves, now keeps the same key across that edit and restores a module cache that is missing the new checksum. Go will fetch what is missing, so it is slower rather than broken.

If you want the old behavior, ask for it rather than pinning an old action. `cache-dependency-path` overrides the file entirely, takes a multi-line list, and is what a monorepo with several modules needs anyway.

```.github/workflows/ci.yml
- uses: actions/setup-go@v7
  with:
    go-version: '1.26'
    # cache defaults to true; this is here to be explicit, not to enable it
    cache: true
    cache-dependency-path: |
      go.sum
      tools/go.sum
      services/api/go.sum
```

## There are no restore keys, so a miss is a total miss

This is the behavior that surprises people who know `actions/cache`, and it is worth stating plainly because no amount of key tuning works around it. The action calls the toolkit restore with a primary key and nothing else. There is no restore-keys list, no prefix fallback, no partial match.

So a Go cache on GitHub Actions is binary. Change one line in the hashed file and you do not get last week's modules plus the delta: you get an empty `GOMODCACHE` and an empty `GOCACHE`, and the job downloads and compiles everything. On a repository where dependencies change weekly that is fine. On one where they change daily, the action is doing almost nothing for you and a hand-rolled `actions/cache` step with `restore-keys` will beat it.

The same all-or-nothing property is why the `ImageOS` component matters more than its size suggests. A runner image roll changes the key for every Go repository in your organization on the same morning, and every one of them takes a full cold build once. If you see a Monday where everything was slow and nothing was deployed, that is usually what it was rather than anything you did.

```.github/workflows/ci.yml
- uses: actions/setup-go@v7
  with:
    go-version: '1.26'
    cache: false

- uses: actions/cache@v4
  with:
    path: |
      ~/go/pkg/mod
      ~/.cache/go-build
    key: go-${{ runner.os }}-1.26-${{ hashFiles('**/go.sum') }}
    restore-keys: |
      go-${{ runner.os }}-1.26-
      go-${{ runner.os }}-
```

> Turn the built-in cache off when you do this. Two steps writing the same two directories under different keys is not twice the cache, it is a race in the post step and an entry whose contents depend on which one finished last.

## The warning that looks like a failure

When the action cannot build a key it does not fail the job. `main.ts` wraps the whole restore in a try block and turns anything thrown into `core.warning`, so the step goes green, the annotation goes in the summary, and the job runs uncached at full cold cost. A pipeline can be silently paying for a cold Go build on every run for months with nothing red anywhere.

Two throws end up behind that warning, and they mean different things. If the dependency file is missing from the workspace root you get the message below. If a `cache-dependency-path` you set matched nothing, you get "Some specified paths were not resolved, unable to cache dependencies" instead, which is the case where a glob is wrong rather than a file being absent.

```Actions log, warning annotation, quoted from kuoss/lethe#83
Restore cache failed: Dependencies file is not found in /home/runner/work/lethe/lethe. Supported file pattern: go.sum
```

> That run predates v6.3.0, which is why the message names `go.sum`; on v6.3.0 and later the same throw prints `go.mod`, because the action interpolates whatever file pattern its version is configured with. The line proves the action could not find a file to hash in the workspace root, and therefore skipped caching. It does not prove that the cache service was unreachable or that the job failed: this is a warning, and the job continued.

## When the entry is written, and when it is not

The save runs as a post step, and its `post-if` is `success()`. A job that fails does not write a cache entry, which is usually what you want and is occasionally the reason a repository never warms up: if your flaky test suite fails half the time, half your runs contribute nothing to the cache.

Two more conditions silently skip the write. If the restore was an exact hit the action logs "Cache hit occurred on the primary key" and saves nothing, which is correct and is why a warm repository shows no upload. And if the toolkit returns a cache id of -1, which happens on a reservation collision or when the job token cannot write, the action traces it and moves on without a warning. That last case is the normal one on a pull request from a fork, where the token is read-only by design.

The other write you cannot see is on a branch. Actions cache entries are scoped: a run restores from its own branch and from the default branch, and a pull request also reads its base branch. If nothing ever runs on your default branch, nothing there is ever warm, and every feature branch starts cold no matter how well the key is written. A small scheduled build on the default branch fixes that for free, and [GitHub Actions cache not restored](/learn/speed/github-actions-cache-not-restored) covers the rest of the scoping rules.

## What to check, in order

- Check the action version before you change anything, because v6.2.0 and v6.3.0 hash different files and that alone explains a cache that went cold with no workflow edit.
- Look for `Cache is not found` in the setup step of a run you expected to be warm. That is the action telling you the primary key missed, and with no restore keys behind it the job is about to do everything from scratch.
- Search the run summary for `Restore cache failed`. It is a warning, so nothing is red, and it means you have been paying full price on every run.
- Confirm something runs on your default branch. A feature-branch-only pipeline has nothing to restore from.
- If your dependencies genuinely change most days, stop tuning `setup-go` and write an `actions/cache` step with restore keys, because partial restores are the only thing that helps at that rate of change.

## FAQ

### Do I need to set cache: true on actions/setup-go?

No. The `cache` input has defaulted to true since v5, so adding it changes nothing. What is worth setting is `cache-dependency-path`, which overrides the file the key hashes and accepts a multi-line list. That is the input a monorepo with several modules actually needs, and the one that lets you keep hashing `go.sum` on a version that now defaults to `go.mod`.

### Does setup-go cache the Go build cache as well as modules?

Yes, both in one entry. It runs `go env GOMODCACHE` and `go env GOCACHE` and caches whatever those report, so the download and the compiled objects are restored together. That pairing is the main reason to prefer it over a hand-rolled cache on the module directory alone, where the modules come back warm and every package still recompiles.

### Why is my Go cache cold when nothing in go.mod changed?

Four things outside your dependency file are in the key: the runner OS, the process architecture, the Go version spec you requested, and on Linux the `ImageOS` value the runner sets. A monthly runner image roll changes that last one for every repository at once, and because the action uses no restore keys the result is a full cold build rather than a partial restore.

### What does "Restore cache failed: Dependencies file is not found" mean?

That the action looked for its dependency file at the workspace root and did not find one, so it skipped caching. It is emitted as a warning by `main.ts`, which catches everything the restore throws, so the step stays green and the job runs at full cold cost. The file it names is whichever pattern that action version uses: `go.sum` before v6.3.0 and `go.mod` from v6.3.0 onward.

### Should I use setup-go caching or actions/cache for Go?

Use `setup-go` unless your dependencies change most days. It picks the right two directories, keys them sensibly and needs no maintenance. Its weakness is that it restores on an exact key match only, with no restore keys, so a repository whose `go.mod` moves daily gets a cold build daily. That is the case where a hand-written `actions/cache` step with prefix restore keys wins.

## References

- [actions/setup-go: cache key assembly in src/cache-restore.ts (verified 2026-09-21)](https://github.com/actions/setup-go/blob/main/src/cache-restore.ts)
- [actions/setup-go v6.3.0 release notes: default Go module caching moves to go.mod (verified 2026-09-21)](https://github.com/actions/setup-go/releases/tag/v6.3.0)
- [GitHub Docs: dependency caching reference, key matching and cache scope (verified 2026-09-21)](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching)
- [kuoss/lethe#83, the run whose warning annotation is quoted on this page (verified 2026-09-21)](https://github.com/kuoss/lethe/issues/83)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
