← Back to blog

Go module cache gotchas that quietly hurt build speed and reproducibility

An architectural look at GOMODCACHE, CI caching, private modules, and the mistakes that make Go builds slower and less predictable.

Jan 18, 20266 min read

GOMODCACHE is one of those Go concepts teams rely on heavily without thinking about it deeply. That usually works until build times become inconsistent, CI jobs start redownloading too much, or a supposedly reproducible environment behaves differently on a new runner.

At that point, the module cache stops being an implementation detail and becomes an operational concern.

This post is about the gotchas that show up when GOMODCACHE is treated casually.

1. GOMODCACHE is not the same as the build cache #

This is the first misunderstanding to fix.

  • GOMODCACHE stores downloaded module source
  • GOCACHE stores compiled build artifacts

Teams often tune one and expect results from the other.

If a CI pipeline repeatedly downloads dependencies, the problem is probably cache persistence or dependency resolution behavior. If the pipeline downloads nothing but still recompiles too much, the issue is likely build cache invalidation.

Treating these as the same cache leads to wasted debugging.

2. A fast local cache can hide non-reproducible builds #

The module cache makes local development feel stable because once dependencies are present, many builds appear deterministic.

But cached success can hide real problems:

  • indirect dependencies missing from go.sum
  • private module access not configured cleanly
  • replace directives depending on local layout
  • stale assumptions about module source availability

Then the build moves to a clean environment and fails.

This is why clean-runner validation matters. A warm GOMODCACHE is useful for speed, but it should not be the only environment you trust.

3. CI cache keys are often too coarse or too narrow #

This is the most common operational mistake.

If the cache key is too coarse, unrelated branches or incompatible states reuse the same module cache and create noisy behavior. If it is too narrow, the cache misses constantly and you lose the benefit entirely.

A practical cache key usually needs to reflect:

  • OS and architecture
  • Go version
  • dependency state, typically go.sum

That balance keeps cache reuse meaningful without turning it into accidental shared state.

4. Sharing one cache across incompatible environments causes drift #

The module cache is portable in many cases, but teams still get burned when they share it too aggressively across environments with meaningful differences.

Be careful when mixing:

  • different Go versions
  • different base images
  • different CPU architectures
  • different private module access settings

The cache may still work often enough to look healthy while creating hard-to-explain misses or odd fetch behavior.

If you want reliable CI performance, partition the cache along real compatibility boundaries.

5. Private modules are where GOMODCACHE turns into a policy problem #

Public module workflows are comparatively easy. Private modules expose the real discipline of the setup.

Common issues:

  • GOPRIVATE not configured correctly
  • credentials available locally but not in CI
  • proxy or checksum expectations mismatched to private repos
  • module download paths depending on developer machine state

When this happens, teams blame the cache. In reality, the cache is just revealing that dependency access is not modeled explicitly.

For private modules, your operating model should be clear:

  • what bypasses the public proxy
  • how auth is injected in CI
  • how clean environments fetch dependencies
  • how failures are surfaced

Without that, GOMODCACHE becomes an unreliable convenience layer instead of a trusted accelerator.

6. Deleting the module cache is a useful test, not a normal habit #

Developers often reach for go clean -modcache as a universal fix. It can help confirm whether the cache is masking a dependency problem, but it should not be your normal recovery strategy.

If regularly deleting the cache is the only way to keep builds healthy, the real issue is usually one of:

  • unstable dependency constraints
  • bad private module configuration
  • toolchain mismatch
  • broken CI cache key design

Wiping the cache hides the operating problem while making performance worse.

7. Container builds often misuse GOMODCACHE #

The classic Docker optimization is to copy go.mod and go.sum early, run go mod download, and preserve that layer. That works well, but only if the Dockerfile structure reflects dependency stability correctly.

The common mistakes are:

  • copying too much source before dependency download
  • invalidating the dependency layer on unrelated file changes
  • expecting module cache reuse across incompatible builders
  • forgetting that private module auth must be handled in the image build path

This is where GOMODCACHE becomes a build-architecture decision, not just a Go flag.

8. Offline or partially connected environments need explicit cache strategy #

Some teams assume the module cache will “just help” in constrained networks. It helps only if the dependency population strategy is deliberate.

Questions worth answering:

  • how is the cache warmed
  • how are module versions approved
  • what happens when a new dependency is introduced
  • is the environment proxy-backed or direct-fetch

Once network reliability is part of the build story, GOMODCACHE becomes a supply path concern.

9. Module source caching is useful, but dependency governance matters more #

A healthy module cache cannot rescue a weak dependency policy.

You still need:

  • controlled version updates
  • review of indirect dependency drift
  • explicit private module boundaries
  • validation on clean runners

The cache is there to reduce repeated work. It should not be carrying the burden of dependency correctness.

10. The best operational model is boring #

What good usually looks like:

  1. GOMODCACHE and build cache are treated separately
  2. cache keys reflect go.sum, Go version, and runtime compatibility
  3. private module access is explicit in CI
  4. container builds preserve dependency layers correctly
  5. periodic clean-runner builds verify reproducibility

That setup is not glamorous, but it removes most of the pain teams attribute to “Go cache weirdness.”

Closing view #

GOMODCACHE is powerful because it makes Go feel fast and stable. The risk is that teams stop thinking about what it actually represents: local dependency state.

If that state is:

  • partitioned correctly
  • populated predictably
  • validated against clean environments
  • aligned with private module policy

then the cache becomes an asset.

If not, it turns into one more layer of hidden build behavior that nobody understands until CI slows down or breaks.

Related posts

More on similar themes