Cache Go Dependencies in CI Pipelines

Khimananda Oli 9 min read Programming and Languages
Cache Go Dependencies in CI Pipelines

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.

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.

CI Job StartRestore CacheCache Hit?Key = hash(go.sum)Cache Missgo mod downloadBuild & TestSave CacheHIT: Skip Download
Figure 1: Correct workflow to cache Go dependencies in CI pipelines prevents redundant network calls on cache hits.

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.

❌ Key: go.mod (Unsafe)Misses transitive dependency updatesRisk of stale or compromised modulesFails integrity verification silently✅ Key: go.sum (Correct)Includes all transitive hashesGuarantees reproducible buildsTriggers refresh on any changeCache Lookup SequenceExact Match (go.sum hash) → Restore Verified ModulesPartial Match (OS + Go Version) → Download Delta Only
Figure 2: Why go.sum is mandatory when you cache Go dependencies in CI pipelines to maintain supply chain integrity.

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.

PlatformNative SupportCache ScopeKey Limitation
GitHub ActionsBuilt-in via setup-goPer-branch, scoped to repo10GB total cap per repo; evicts LRU
GitLab CIManual via cache/artifactsConfigurable (branch/global)No automatic hash detection; manual keys
CircleCIsave_cache / restore_cacheProject-wide with prefix matchingImmutable caches; must version keys manually
JenkinsPlugin-dependent or workspaceAgent-local or shared storageEphemeral 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 -modcache or set GOMODCACHE to 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 constituent go.sum files plus go.work.sum. Missing one invalidates the entire workspace cache guarantee.
Cache Not Working?Check: go.sum in key?NOYESFix: Add hashFiles()Use go.sum, not go.modNext: Check PathsVerify GOMODCACHE valueStill failing?Check Go version in keyVerify Vendor Mode Off
Figure 3: Troubleshooting flowchart for resolving issues when you cache Go dependencies in CI pipelines.

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.

Frequently Asked Questions

Use the official actions/setup-go action with cache set to true. This automatically caches the module download directory and build cache based on your go.sum file hash, requiring zero manual path configuration for standard Go projects in 2026.

Yes. Restoring cached modules typically saves thirty to ninety seconds per job by skipping network downloads. The benefit compounds across matrix builds where multiple jobs share identical dependency trees and checksums.

Cache GOMODCACHE and GOCACHE. The former stores downloaded module source code while the latter holds compiled package objects. Caching both ensures you skip redundant downloads and recompilation during subsequent pipeline runs.

Yes. Override default keys using runner.os and hashFiles of go.sum. Append branch names or weekly timestamps to force periodic refreshes when dependencies update frequently without changing the sum file checksum.

Verify go.sum exists at the repository root and paths match environment variables. Check that setup-go version supports automatic caching. Mismatched OS runners or corrupted lockfiles often cause silent cache misses during restoration.

Generally no. Vendor directories bloat cache size significantly compared to module caches. Modern Go tooling prefers module proxy downloads which are smaller, faster to restore, and guaranteed consistent via cryptographic checksums in go.sum files.

GitLab requires explicit cache key definitions using files like go.sum. You must manually specify untracked paths for GOMODCACHE since GitLab lacks native Go awareness unlike GitHub Actions setup-go integration available in 2026.

The cache key invalidates automatically because most setups hash go.sum content. CI downloads fresh dependencies matching the new checksum then saves an updated cache entry for future jobs using that specific dependency state.

Avoid caching test binaries as they become stale quickly and consume storage quotas. Focus solely on GOMODCACHE and GOCACHE which remain valid longer and provide maximum time savings without risking incorrect test execution results.

Configure cache cleanup policies in your CI provider settings. Most platforms evict unused entries after seven days. For self-hosted runners, set GOMODCACHE max size via environment variables to prevent disk exhaustion over time.

Yes. Authenticate via git credentials or netrc before caching. Private module sources download through authenticated proxies then cache identically to public ones. Ensure secrets never leak into cache keys or stored artifact metadata.

No. They complement each other. Docker caches filesystem layers including compiled binaries while Go module caching persists across container rebuilds. Use both to minimize network calls during development and image build stages independently.

Enable verbose output in setup-go or cache actions. Look for cache hit or miss messages referencing specific keys. Compare expected versus actual hash values to identify whether go.sum changed or paths were misconfigured.

Go 1.20 improved GOCACHE pruning and module graph handling making CI caching more reliable. By 2026 all maintained versions include these optimizations so upgrade if using older releases experiencing frequent cache corruption issues.

Public repositories get free unlimited caching on major platforms. Private repos may incur storage fees exceeding monthly limits. Monitor usage dashboards regularly and implement retention policies to avoid unexpected charges from accumulated stale cache entries.