
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Slow builds kill developer momentum, and for Go teams, the bottleneck is often redundant module downloads. If you do not cache Go dependencies in CI pipelines, every run wastes minutes fetching identical packages from upstream proxies. This latency compounds across matrix builds and multiple branches, turning a ten-minute feedback loop into a thirty-minute wait. The fix is deterministic caching keyed on your dependency manifest, ensuring runners reuse verified modules instead of re-downloading them.
go.sum file to generate a unique cache key and map it to the output of go env GOMODCACHE. This ensures that cached modules are restored only when dependencies remain unchanged, preventing stale builds while reducing download time by over 60% on average.Implementing this correctly requires understanding Go's specific module storage layout, which differs from other language ecosystems. Unlike Node.js or Python, Go stores modules in a content-addressable format within a global cache directory rather than a local vendor folder. Misconfiguring this path or using an imprecise cache key leads to either perpetual cache misses or, worse, corrupted builds that pass locally but fail in production. For teams managing microservices or monorepos, getting this right is foundational to maintaining velocity. You can see how this fits into broader automation strategies in our guide on build pipeline automation best practices.
How do you configure GitHub Actions to cache Go dependencies?
GitHub Actions provides the most streamlined experience for Go caching through the official actions/setup-go action. Since version 4, this action includes built-in caching that automatically detects your Go version and hashes your dependency files. However, relying solely on defaults can be risky for complex projects. Explicit configuration gives you control over fallback behavior and multi-module repositories.
Standard single-module configuration
For a typical repository with a single go.mod at the root, the setup is straightforward. The critical detail is enabling the cache explicitly and verifying the post-job cleanup step runs successfully.
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache-dependency-path: go.sum
# cache is true by default in v4+, but explicit is safer
- name: Download dependencies
run: go mod download
- name: Run tests
run: go test -race ./... The cache-dependency-path parameter accepts glob patterns. If your project uses a workspace or has nested modules, specify all relevant sum files to ensure the cache invalidates correctly when any dependency changes. Without this, updating a sub-module might leave you with a stale root cache.
Handling cache restoration failures
A common mistake is assuming the cache will always restore cleanly. Network blips or storage quota limits can cause silent failures. Always verify the cache state before proceeding. In custom workflows where you cannot use the built-in action, use actions/cache directly with precise paths derived from the environment:
- name: Get Go cache paths
id: go-cache-paths
run: |
echo "gomodcache=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
echo "gobuildcache=$(go env GOCACHE)" >> $GITHUB_OUTPUT
- name: Go Mod Cache
uses: actions/cache@v4
with:
path: ${{ steps.go-cache-paths.outputs.gomodcache }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-mod- This approach separates the module cache (downloaded packages) from the build cache (compiled artifacts). Caching both is beneficial, but they serve different purposes. The module cache saves network I/O; the build cache saves CPU cycles. For teams auditing infrastructure costs, distinguishing these helps optimize storage spend, a topic we explore further in cloud cost optimization tactics.
Why is go.sum the correct cache key for Go modules?
Using go.sum as the cache key is not arbitrary; it is a security and correctness requirement. The go.sum file contains cryptographic hashes of every module version required by your project, including transitive dependencies. When you run go mod download, the Go toolchain verifies each downloaded zip against these hashes. If you were to key your cache on go.mod alone, you would miss updates to indirect dependencies that do not change the direct requirement list but do alter the checksum file.
Caching based on go.mod creates a subtle vulnerability: a new transitive dependency could be released with the same version tag but different content (a compromised proxy scenario). Your CI would restore the old cached zip, bypassing the verification step that would normally catch the mismatch. By keying on go.sum, any change in the dependency graph—direct or indirect—forces a cache miss and triggers a fresh, verified download.
This strictness aligns with supply chain security principles. In regulated environments where audit trails matter, deterministic caching proves that the exact binaries tested are the ones deployed. Never use floating keys like timestamps or branch names for Go modules. The performance gain from avoiding unnecessary downloads should never come at the cost of reproducibility. For deeper context on securing your pipeline inputs, review our article on handling secrets in CI/CD pipelines safely.
How does Go module caching differ across CI platforms?
While the underlying mechanism—hashing go.sum and storing the GOMODCACHE directory—is universal, each CI platform implements storage and restoration differently. Understanding these nuances prevents platform-specific pitfalls that waste debugging time.
| Platform | Native Support | Cache Scope | Key Limitation |
|---|---|---|---|
| GitHub Actions | Built-in via setup-go | Per-branch, scoped to repo | 10GB total cap per repo; evicts LRU |
| GitLab CI | Manual via cache/artifacts | Configurable (branch/global) | No automatic hash detection; manual keys |
| CircleCI | save_cache / restore_cache | Project-wide with prefix matching | Immutable caches; must version keys manually |
| Jenkins | Plugin-dependent or workspace | Agent-local or shared storage | Ephemeral agents lose cache without external store |
In GitLab CI, you must explicitly define the cache policy. Using policy: pull-push on main and policy: pull on feature branches prevents cache thrashing. CircleCI’s immutable cache model means you should append a version number or epoch to your key when you need to force a clear, as old keys persist indefinitely until manually purged. Jenkins users running ephemeral containers should mount a persistent volume for GOMODCACHE or use an S3-backed cache plugin; otherwise, every build starts cold regardless of configuration.
What are common pitfalls when caching Go modules in CI?
Even with correct configuration, several edge cases break caching silently. Identifying these early saves hours of confusing build failures.
- Vendor directory conflicts: If you commit a
vendor/directory, disable module caching entirely. Go prioritizes vendor over the module cache, making the cache dead weight that consumes storage quota. Either vendor fully or cache fully—never both. - Go version mismatches: Module cache layouts can change between major Go versions. Always include the Go version in your cache key (e.g.,
go-1.23-mod-hash). Restoring a 1.22 cache into a 1.23 environment causes mysterious compilation errors. - Private module authentication: Cached private modules retain their original fetch metadata. If your GOPRIVATE configuration or git credentials change between jobs, the cache may contain modules that fail validation. Ensure auth setup happens before cache restoration.
- Disk space exhaustion: Go’s module cache grows monotonically. On self-hosted runners with limited disk, implement periodic cleanup using
go clean -modcacheor setGOMODCACHEto a dedicated partition. Monitor disk usage as part of your observability stack, similar to approaches in Prometheus metrics monitoring fundamentals. - Workspace mode complexity: Go workspaces (
go.work) aggregate multiple modules. Hash all constituentgo.sumfiles plusgo.work.sum. Missing one invalidates the entire workspace cache guarantee.
When should you avoid caching Go dependencies entirely?
Caching is not universally beneficial. Security-sensitive pipelines, such as those producing signed release artifacts or handling cryptographic libraries, sometimes warrant cold builds to guarantee provenance. In these cases, the assurance that every byte was freshly fetched and verified outweighs the two-minute time savings. Similarly, if your dependency footprint is tiny (under 50MB total), the overhead of cache save/restore operations may exceed the download time itself. Profile your actual build times before optimizing blindly.
Another exception is CI environments with unreliable or slow cache storage backends. If your runner’s cache restoration consistently takes longer than a fresh go mod download due to network topology or storage contention, disable caching and invest in a faster proxy or artifact mirror instead. Pragmatism beats dogma; measure first, then optimize.
Optimize Your Go Pipeline Today
Properly configuring your system to cache Go dependencies in CI pipelines transforms sluggish feedback loops into responsive development cycles. Start by auditing your current cache keys against go.sum, verify your GOMODCACHE paths match your Go version, and monitor cache hit rates as a first-class metric. The cumulative time saved across your team compounds into significant capacity gains. If your Go builds still feel slow after implementing these patterns, or if you need help designing a compliant, audit-ready CI/CD architecture, reach out to discuss your pipeline optimization needs.