How do I share the Go build cache across CI runs and runners?
To share the Go build cache across CI runs, keep two directories between jobs: GOMODCACHE for downloaded modules and GOCACHE for compiled packages and test results. Key them so that the build cache gets saved again on new commits, and store them where every runner can reach them. On GitHub Actions, actions/setup-go does part of this. On GitLab CI, cache: with a go.sum key and fallback keys does it. For machines that do not share disks, a GOCACHEPROG program can serve the build cache remotely. Dagger meets the same requirements in one place. Its official Go module mounts both caches as cache volumes, and Cloud Checks run it on Cloud Engines that share their cache.
What a shared Go cache must do
A Go cache setup for CI has four requirements. Each one comes from how the go command and the CI platforms work:
- Keep both caches. The
gocommand keeps downloaded modules inGOMODCACHEand build outputs inGOCACHE. It also “caches successful package test results” inGOCACHE. If you keep only the module cache, every job compiles every package again and runs every test again. - Keep the build cache current. A build cache that CI saved once and never updated gets older with each commit. Each run then compiles more packages again.
- Stay correct. The
gocommand’s own cache keys already account for “changes to Go source files, compilers, compiler options, and so on”, and the cache is “safe for concurrent invocations of the go command” (go help cache). The CI cache key only decides which snapshot a job gets. It does not make the build correct or incorrect. - Reach every runner. A cache on one machine’s disk does not help a job that runs on another machine.
Cache Go with actions/setup-go on GitHub Actions
actions/setup-go caches Go modules and build outputs by default (cache: true). Since v6, it computes the cache key from go.mod in the repository root. To use go.sum, or to cover several Go modules, set cache-dependency-path, which accepts globs (setup-go README):
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache-dependency-path: '**/go.sum'
This meets “keep both caches”. By default, it does not keep the build cache current. The setup-go docs say that the GOCACHE cache “is not updated based on source changes”. You can add **/*.go to cache-dependency-path, but the docs warn that this “can create new caches on many commits”. When the key matches, the job restores the files and saves nothing new, because you cannot change the contents of an existing cache.
To save a new build cache on each run, turn off the built-in cache. Then use actions/cache with a unique key and a restore-keys prefix. setup-go gives the two cache paths as outputs:
- uses: actions/setup-go@v7
id: go
with:
go-version-file: go.mod
cache: false
- uses: actions/cache@v5
with:
path: |
${{ steps.go.outputs.GOMODCACHE }}
${{ steps.go.outputs.GOCACHE }}
key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}-${{ github.sha }}
restore-keys: |
go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}-
go-${{ runner.os }}-
restore-keys restores the most recent cache with the same prefix (cache key matching). Two rules decide which runs can share it:
- Seed the cache from the default branch. A cache that a pull request saves is scoped to its merge ref, and other pull requests cannot restore it. Run the same job on
pushtomain. - Save from one job. For parallel jobs, setup-go recommends that you “create the cache in only one build and restore it in the other builds”.
GitHub stores the caches, so every runner in the repository can restore them.
Cache Go with cache keys on GitLab CI
GitLab caches only paths inside the project directory. Caches “can’t link to files outside it”. Move both Go caches into $CI_PROJECT_DIR, then give each cache its own key:
variables:
GOMODCACHE: $CI_PROJECT_DIR/.go/pkg/mod
GOCACHE: $CI_PROJECT_DIR/.cache/go-build
test:
image: golang:1.26
cache:
- key:
files: [go.sum]
paths: [.go/pkg/mod]
- key: go-build-$CI_COMMIT_REF_SLUG
fallback_keys: [go-build-$CI_DEFAULT_BRANCH]
paths: [.cache/go-build]
script:
- go test ./...
The module cache changes only when go.sum changes, so cache:key:files fits it. The build cache uses one key for each branch, and each job saves a new copy under that key. A new branch starts from the default branch’s cache through fallback_keys, which takes up to five keys.
Three settings decide who shares what:
- Policy. The default
pull-pushdownloads the cache and uploads it again after each job. Setpolicy: pullon jobs that only read. - Protected branches. By default, “protected and non-protected branches do not share the cache”. GitLab adds a
-protectedor-non_protectedsuffix to each key, fallback keys included. - Runners. A cache lives where GitLab Runner is installed. To share it across several runners, use distributed caching in S3 or a shared network directory.
Share the build cache remotely with GOCACHEPROG
Since Go 1.24, the GOCACHEPROG environment variable names a program that implements the build and test cache for the go command. The go command sends get and put requests for single cache entries over a JSON protocol. The program can store the entries anywhere, such as in remote storage. Depot’s depot gocache is one such program.
This meets “reach every runner” without archive files. Each job gets only the entries its build asks for, and it writes its new entries back, so the cache stays current. GOCACHEPROG replaces the build cache only. You still cache GOMODCACHE another way, and you run or buy the program and its storage.
Where the hand-built setup strains
The setups above work, but each requirement lives in a different place:
- The keys are a design you maintain. A
go.sumkey stops build cache updates. A commit key needsrestore-keysorfallback_keys, and one job that writes the cache. - Every job transfers the whole archive. GitHub and GitLab restore and save whole directories, so a larger build cache is a larger download and upload in each job.
- The limits are the platform’s. GitHub removes caches that no run used in 7 days and limits a repository to 10 GB by default. GitLab separates protected branches, and it needs S3 or a network directory for several runners.
- The CI caches stay in CI. The setup-go and GitLab caches do not reach the laptops where developers run the same tests.
Keep both caches with the Dagger Go module
The Dagger Go module runs go test and go generate for every Go module in the repository. A Dagger module is a package of functions that build and test a project. The Go module mounts GOMODCACHE and GOCACHE as cache volumes, which are directories whose contents persist across runs on the Dagger engine. The engine is the process that runs the module’s containers. You write no cache configuration.
Complete the Quickstart in the repository first. Then install the module:
dagger module install dagger.io/go
dagger.io/go ➡️ https://github.com/dagger/go#v0.3.1
Installed module "go" in /work/dagger.toml
Run its checks. A check is a function that passes or fails, such as a test run:
dagger check go
== CHECKS == ✔ 2 passed
✔ dag://?check=go/packages/generate/stale 0.5s OK
✔ dag://?check=test&go-package=. 4.8s OK
== TESTS ==
✔ example.com/inventory 1 passed
✔ 1 passed
The module mounts the module cache at /go/pkg/mod and the build cache at /root/.cache/go-build, and it sets GOMODCACHE and GOCACHE to those paths. Every Go command that the module runs uses the same two volumes.
This guide’s project has one package that imports the etcd v3 client. The first dagger check go took 6.5 seconds, and its test check took 4.8 seconds. After a one-line change to the package source, dagger check go took 1.6 seconds, and the test check took 0.6 seconds. That is one run on one machine, not a benchmark.
Keep the cache current and correct with Dagger
The cache volumes have no key to design. Each run reads from the same volumes and writes its new entries back to them, so the build cache stays current on every commit. The go command’s own cache keys keep the build correct, the same as on a CI runner. Concurrent checks share the volumes, which the Go cache allows.
Dagger also caches each check’s result by exactly the files the check saw (Daggerize a Go Project). In this guide’s project, a rerun with no changes took 1.5 seconds for dagger check go, and the test check took 0.4 seconds.
Share the cache across runners with Cloud Checks
Cloud Checks run your checks on Cloud Engines on each push and pull request, with no CI workflow file. Cloud Engines are Dagger engines that Dagger Cloud runs for you, with a distributed cache. Turn on Cloud Checks from the project root:
dagger cloud checks on
The command needs a Dagger Cloud account and a connected GitHub repository, and it tells you how to complete what is missing. After that, each push runs the same dagger check that you run locally, with the same Go module and the same cache volumes.
According to the changelog, Cloud Engines share their cache through copies of a shared cache disk, and these copies are best effort. A version that shares single results is in the changelog’s Coming soon section and is not released.
To share one cache between developers and CI, run local commands on the same engines with dagger check --engine=cloud. To use your own machine instead, point every runner at one engine, for example with --engine=ssh://build-host (remote execution).
| Requirement | setup-go | GitLab cache: |
GOCACHEPROG |
Dagger Go module |
|---|---|---|---|---|
| Keep both caches | Yes, by default | Yes, after you move both into the project directory | Build cache only | Yes, two cache volumes |
| Keep the build cache current | Only with a custom key or source globs | With a branch key and fallback keys | Yes, entry by entry | Yes, every run writes back |
| Reach every runner | GitHub cache storage | S3 or a network directory for several runners | The program’s storage | Cloud Engines, or one shared engine |
Bottom line: Keep both GOMODCACHE and GOCACHE, and key the build cache so that it gets saved again on new commits. Store it where every runner can reach it: setup-go with a custom key on GitHub Actions, branch keys with fallbacks and S3 on GitLab CI, or GOCACHEPROG for remote entries. The Dagger Go module keeps both caches in cache volumes with no keys to maintain, and Cloud Checks run it on Cloud Engines that share their cache.
Related guides
- How do I run Go tests, lint, and go generate the same way locally and in CI?: set up the Go module and its checks for a whole project.
- How do I cache Rust builds in CI without rebuilding everything?: the same question for Cargo builds.
- Can Dagger lower CI costs?: how caching and Dagger Cloud pricing affect the CI bill.