Go Builds on GitHub Actions: Sizing and Caching

Cache GOMODCACHE and GOCACHE with WarpBuilds/cache keyed on go.sum, then size warp- runners from 4 to 16 vCPU for go build and go test -race jobs.

Last verified:

To cache Go builds on GitHub Actions, persist two directories between jobs: the module cache (GOMODCACHE, default ~/go/pkg/mod) and the build cache (GOCACHE, default ~/.cache/go-build), keyed on the hash of go.sum. On WarpBuild runners, the WarpBuilds/cache action is a drop-in replacement for actions/cache@v4 and stores both in WarpBuild's cache backend, which is enabled by default on all Linux runners.

Caching solves the download and recompile problem. Runner sizing solves the parallelism problem, because go build and go test both scale with core count up to the width of your package graph. This page covers the workflow configuration, the sizing decision between 4, 8, and 16 vCPU runners, the four bottlenecks that dominate Go pipelines, and the cost math against GitHub-hosted runner list prices.

Overview

The Go toolchain maintains two caches, and both start empty on every fresh GitHub Actions virtual machine.

GOMODCACHE holds downloaded module source. Without it, every job re-fetches every dependency in go.sum from the module proxy before compiling anything. GOCACHE holds compiled package artifacts. Without it, go build recompiles every package in the dependency graph from scratch. Since Go 1.20, the Go distribution no longer ships precompiled archives for the standard library, so a cold GOCACHE also means recompiling the standard library packages your code imports. Run go env GOMODCACHE GOCACHE on any machine to see where both live.

Hosted runners are ephemeral by design, so the fix is a cache action that saves and restores those two directories across jobs. Key the cache on hashFiles('**/go.sum') so it rolls exactly when the module set changes, and add a restore-keys prefix so a dependency bump restores the previous cache instead of starting cold.

The second half of the problem is hardware. go build compiles independent packages in parallel, bounded by GOMAXPROCS, which defaults to the vCPU count. go test goes further: it builds and runs the test binaries of different packages in parallel, controlled by the -p flag, which also defaults to GOMAXPROCS. A repository with a wide package graph will keep 8 or 16 cores busy; a repository with 15 packages will leave most of a 32-core machine idle.

For Go, the Linux x64 sizes from 4 to 16 vCPU cover almost every repository, and moving a job onto them is a one-line change to runs-on. The cloud runners documentation lists the full catalog with per-minute rates, which also appear on the pricing page.

Configuration

The explicit form uses WarpBuilds/cache to save and restore GOMODCACHE and GOCACHE directly. This workflow builds and race-tests a Go module on an 8 vCPU WarpBuild runner:

name: go-ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: warp-ubuntu-latest-x64-8x
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.24"
          cache: false

      - name: Restore Go module and build caches
        uses: WarpBuilds/cache@v1
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-

      - run: go build ./...
      - run: go vet ./...
      - run: go test -race ./...

cache: false on the setup step matters: it disables the built-in caching that would otherwise write to GitHub Actions Cache, so the WarpBuild cache step owns both directories. The cache-hit output is available if you want to skip a warm-up step on an exact key match.

The shorter form replaces the upstream setup action with WarpBuilds/setup-go, which installs Go and routes its toolchain and dependency caching through WarpBuild Cache automatically. Caching is on by default, keyed on go.mod; point it at go.sum for a key that tracks dependency versions:

- uses: WarpBuilds/setup-go@v6
  with:
    go-version: "1.24"
    cache-dependency-path: "**/go.sum"

Two behaviors from the caching documentation are worth knowing before you rely on either form. The cache is scoped to key, version, and branch, so a cache saved on a feature branch is separate from the one on main; seed the cache from main and let branch builds restore it through restore-keys. Entries expire 7 days after last use, so a repository with weekly activity stays warm and an abandoned branch stops costing storage on its own.

Cache usage is metered, and the rates are small enough to state exactly: storage is $0.20 per GB-month and each write or restore operation is $0.0001. A Go repository with a 2GB combined module and build cache and 3,000 cache operations a month adds about $0.70 to the bill.

Sizing

WarpBuild Linux x64 runners scale from 2 to 32 vCPU with 4GB of memory per core. The three sizes that matter for Go are 4x, 8x, and 16x:

Runner labelvCPUMemoryStoragePrice per minute
warp-ubuntu-latest-x64-4x416GB150GB SSD$0.008
warp-ubuntu-latest-x64-8x832GB150GB SSD$0.016
warp-ubuntu-latest-x64-16x1664GB150GB SSD$0.032

Match the size to package-level parallelism, because that is what the Go toolchain can actually use:

warp-ubuntu-latest-x64-4x fits a single service with a modest dependency graph, roughly under 50 packages including tests. go build ./... on a warm cache finishes quickly at this width, and a larger machine would spend most of its cores idle.

warp-ubuntu-latest-x64-8x is the default for most Go repositories. Monorepos with several services, or any repository running go test -race ./..., benefit from the extra cores for parallel package tests and from 32GB of memory for race-instrumented binaries.

warp-ubuntu-latest-x64-16x earns its rate when the package graph is wide enough to keep 16 cores busy: large monorepos, integration test suites that run many package test binaries concurrently, or race-detector runs that are memory-hungry at -p 8. It is also the practical choice for cross-compilation matrices collapsed into a single job, since each GOOS/GOARCH target is an independent compile that can run alongside the others.

A useful calibration step: run your workflow once on 8x with go test -race ./... and watch utilization. If the test phase holds all 8 cores near full and memory stays comfortable, 8x is right. If cores sit idle, drop to 4x and keep the same wall time at $0.008 instead of $0.016 per minute.

Worked cost model

GitHub publishes per-minute list prices for its hosted runners on the GitHub Actions billing page and at github.com/pricing. Checked on 2026-08-13, the standard ubuntu-latest runner for private repositories (2 vCPU) is $0.006 per minute, and larger Linux runners are $0.012 for 4 vCPU, $0.022 for 8 vCPU, and $0.042 for 16 vCPU.

MachinevCPUPer-minute rateSource
GitHub-hosted standard Linux2$0.006GitHub list price, checked 2026-08-13
GitHub-hosted Linux larger runner8$0.022GitHub list price, checked 2026-08-13
warp-ubuntu-latest-x64-2x2$0.004WarpBuild pricing
warp-ubuntu-latest-x64-8x8$0.016WarpBuild pricing

Take a concrete team: a Go monorepo running 1,500 jobs a month, averaging 6 minutes per job on an 8 vCPU machine with a warm cache. That is 9,000 runner minutes a month.

ScenarioRateMonthly minutesMonthly cost
GitHub-hosted Linux larger runner, 8 vCPU$0.022/min9,000$198.00
warp-ubuntu-latest-x64-8x, 8 vCPU$0.016/min9,000$144.00
Cache storage and operations (2GB, 3,000 ops)see aboven/a$0.70

Stated as list-price arithmetic: warp-ubuntu-latest-x64-8x (8 vCPU, 32 GB) costs $0.016 per minute against $0.022 per minute for the 8-core Linux larger runner (8 vCPU, 32 GB): 27 percent lower list price. GitHub list price checked on 2026-08-13. At the smallest size, warp-ubuntu-latest-x64-2x (2 vCPU, 8 GB) costs $0.004 per minute against $0.006 per minute for ubuntu-latest (2 vCPU, 8 GB on private repositories): 33 percent lower list price. GitHub list price checked on 2026-08-13. Every rate above is on the pricing page.

Full rates for every size and platform are on the pricing page.

Bottlenecks

Four bottlenecks account for most slow Go pipelines on GitHub Actions.

Cold module download. Without a restored GOMODCACHE, every job pulls the full module graph from the module proxy before the first compile starts. The fix is the cache configuration above. Verify it worked by checking the job log: a warm job shows no go: downloading lines.

Cold build cache. A restored GOMODCACHE with an empty GOCACHE still recompiles every package, including the standard library packages your code imports. This is the more expensive of the two cold starts, and it is why the cache step lists both directories. If your key rolls on every commit, you are paying this cost on every build; key on go.sum and use restore-keys so only dependency changes invalidate the compiled artifacts.

Race-detector test runs. The race detector instruments every memory access, so go test -race binaries need several times the memory of uninstrumented runs and execute markedly slower; the Go race detector documentation describes the overhead ranges. On small runners this shows up as swapping or an out-of-memory kill partway through the test phase. Move race runs to 8x or 16x, where the 4GB-per-core ratio absorbs the overhead, and keep a separate fast job running go test ./... without the flag for pull request feedback.

Cross-compilation matrices. Release workflows that build for several GOOS/GOARCH pairs multiply the cold-cache problem, because each target populates its own build cache entries. Give each matrix leg its own cache key so restores stay relevant:

strategy:
  matrix:
    include:
      - goos: linux
        goarch: amd64
      - goos: linux
        goarch: arm64
      - goos: darwin
        goarch: arm64
steps:
  - uses: WarpBuilds/cache@v1
    with:
      path: |
        ~/go/pkg/mod
        ~/.cache/go-build
      key: go-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}

For Linux ARM64 targets you can also skip cross-compilation and build natively on Linux ARM64 runners, which start at $0.003 per minute for 2 vCPU. If the matrix feeds container images, multi-platform Docker builds covers the buildx side of the same problem.

When a job is slow and the cause is unclear, WarpBuild's CI observability shows OpenTelemetry-based system metrics from the runner agent correlated with GitHub Actions job logs, which separates a CPU-bound compile from a job waiting on network or disk. For interactive debugging, the Action Debugger pauses a workflow and opens an SSH session on the runner, so you can run go env and inspect cache directories on the machine itself. Broader workflow-level tactics are collected in the guide on speeding up GitHub Actions.

Proof

The cheapest check is your own repository.

The same sizing-plus-caching approach applies to other compiled toolchains; the Rust on GitHub Actions page walks the equivalent decisions for Cargo.

FAQ

Should a Go cache key hash go.mod or go.sum?

Hash go.sum. It changes whenever any dependency in the module graph changes version, so the cache key rolls at exactly the right moments. WarpBuilds/setup-go keys on go.mod by default; set cache-dependency-path to **/go.sum for a tighter key.

Does caching GOCACHE still matter once GOMODCACHE is cached?

Yes. GOMODCACHE only saves the source download. GOCACHE stores compiled package artifacts, which is where most of the time in a cold go build goes, including the standard library packages your code imports.

What runner size does go test -race need?

Race-instrumented test binaries need several times the memory of a normal test run. Start on warp-ubuntu-latest-x64-8x with 8 vCPU and 32GB, and move to warp-ubuntu-latest-x64-16x when package-level parallelism keeps 8 cores busy or memory peaks near the limit.

What changes when a Go workflow moves to WarpBuild?

One line per job: the runs-on label.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.