logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·30.10.26
← Back to news

Scaling Golang CI by Replacing actions/setup-go

Original article
cloudx.ai|51 points|7 comments|by peterldowns|Sep 16, 2026

Scaling Go CI: Moving Beyond actions/setup-go

By Lukas Schwab and Peter Downs | CloudX Engineering | Sep 16, 2026

We have discovered a method to significantly accelerate parallel Golang continuous integration (CI) workflows by optimizing how we leverage the Golang build cache. By swapping GitHub's standard actions/setup-go with a custom, drop-in replacement, we managed to slash our test job runtimes by 69%.

To help others achieve these gains, we are open-sourcing our solution: cloudx-io/setup-go.

The Problem: Why the Official Action Struggles

While actions/setup-go is the recommended path for most, it introduces performance bottlenecks in parallel environments. Specifically, it causes parallel jobs to interfere with one another and frequently restores outdated stale cache values.

In our monorepo—which typically runs a few parallel Go jobs (linting, testing, and building)—our analysis revealed that 86% of the work performed by the default action is entirely redundant.

Takeaway: If you are managing a Go project of moderate complexity, you can likely expect similar performance boosts.


Our Philosophy: The Pursuit of Fast CI

At CloudX, our velocity is increasing as we ship more features. This isn't accidental; it is the result of heavy investment in our developer tooling. The "software factory" relies on CI workflows to ensure stability.

Our CI Objectives

  • Reliable test execution.
  • Rapid feedback loops.
  • Zero production regressions.

Our internal benchmark is strict: when a developer pushes code, they should receive a definitive answer regarding its acceptability (build status, test results, and linting compliance) within 90 seconds.

To achieve this, we focus on algorithmic improvements. While we already utilize Warp Build for high-performance, cost-effective compute, we realized that actions/setup-go had become a scaling bottleneck.


Technical Analysis: How actions/setup-go Fails

The official action relies on actions/cache to manage the Go module and build caches. Ideally, this allows artifacts from one run to be reused in subsequent runs. However, the cache key construction is the root of the problem:

Key=setup-go-os-arch-goVersion-hashFiles(’**/go.mod’)\text{Key} = \text{setup-go-}\text{os-}\text{arch-}\text{goVersion-}\text{hashFiles('**/go.mod')}Key=setup-go-os-arch-goVersion-hashFiles(’**/go.mod’)

The Flaw in the Logic

This key is far too static. In a typical development cycle, the OS, architecture, Go version, and go.mod file rarely change. Consequently:

  1. The first job generates the key and saves the cache.
  2. Every subsequent run loads that same initial value.
  3. As the application code evolves, the restored build archives become less relevant, forcing the runner to do more work from scratch.
  4. Test outputs become stale, leading to unnecessary re-testing.

The Parallel Race Condition

When multiple jobs run in parallel, they compete to write to the same cache key. Because the final state of the Go cache depends on the specific commands executed, the jobs "race" to overwrite the cache.

Example of a problematic workflow:

# Simplified .github/workflows/go.yaml
jobs:
  test:
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
      - run: go test ./...

  lint:
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
      - uses: golangci/golangci-lint-action@v9

Understanding the Go Toolchain Cache

Linting, building, and testing are essentially pure functions of the source code: f(source code)→outputf(\text{source code}) \rightarrow \text{output}f(source code)→output

If the input hasn't changed, we should be able to reuse the output (memoization). Go handles this via two primary directories:

Cache TypeEnv VariableDefault Linux PathPurpose
Module CacheGOMODCACHE$GOPATH/pkg/modStores downloaded dependency source code.
Build/Test CacheGOCACHE~/.cache/go-buildStores compiled artifacts and test results.

Module Cache

The GOMODCACHE simply stores source code organized by package identifiers. You can see this by inspecting the directory:

$ head -5 $(go env GOPATH)/pkg/mod/github.com/peterldowns/pgmigrate@v0.3.1/pgmigrate.go
package pgmigrate
import (
    context
    database/sql

Build and Test Cache

Both build and test outputs reside in GOCACHE. Go hashes the full set of inputs to create a key, then stores the result in a prefixed subdirectory structure:

$ tree $(go env GOCACHE) | head -6
~/.cache/go-build
├── 00
│   ├── 000131ed61b57fbbb4ad26f5862b2aa

Cache Structure Diagram (Visual representation of the hashed directory structure in GOCACHE)