How Do I Run Go Race Detector Tests in GitHub Actions?
Run go test -race on a job sized for the overhead, since race detection raises memory and run time. Split a fast unit job from a slower race job.
Last verified:
Answer
Run the suite with go test -race ./... on a job sized for the overhead, because race detection multiplies both memory use and run time. The Go race detector documentation states that for a typical program memory usage may increase by 5x to 10x and execution time by 2x to 20x, which means the same tests that fit a 4 vCPU runner without the flag can exhaust it with the flag on.
That is why the working shape is two jobs rather than one. A fast job runs go test ./... and gates pull requests, and a second job runs the race suite on a larger label without holding merges hostage to the slower run.
name: test
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "0 6 * * *"
jobs:
unit:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v5
- uses: WarpBuilds/setup-go@v6
with:
go-version: "1.24"
cache-dependency-path: "**/go.sum"
- run: go test ./...
race:
if: github.event_name != 'pull_request'
runs-on: warp-ubuntu-latest-x64-16x
steps:
- uses: actions/checkout@v5
- uses: WarpBuilds/setup-go@v6
with:
go-version: "1.24"
cache: false
- uses: WarpBuilds/cache@v1
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-race-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-race-
- run: go test -race -p 8 -timeout 30m ./...
env:
GORACE: halt_on_error=1Teams that want race coverage before merge can drop the if condition and instead leave the race job out of the required status checks, so it reports on the pull request without blocking it.
WarpBuilds/setup-go@v6 installs Go and caches modules and build outputs, with caching on by default keyed on the hash of go.mod (setup actions documentation). The race job sets cache: false and takes explicit control of the cache instead, for the reason in the cache section below.
Detail
Sizing the race job against the Linux ladder
go test runs the test binaries of different packages in parallel, bounded by -p, which defaults to GOMAXPROCS and therefore to the vCPU count. Every one of those binaries carries the race multiplier at the same time, so peak memory is the per-binary peak times the parallelism, and a wide package graph is where out-of-memory kills come from.
Here is the memory budget per parallel test binary at the default -p on the Linux x64 catalog, with rates from the pricing page and shapes from the cloud runners documentation, checked on 2026-08-13:
| Runner label | vCPU | RAM | Default -p | RAM per parallel binary |
|---|---|---|---|---|
warp-ubuntu-latest-x64-4x | 4 | 16 GB | 4 | 4 GB |
warp-ubuntu-latest-x64-8x | 8 | 32 GB | 8 | 4 GB |
warp-ubuntu-latest-x64-16x | 16 | 64 GB | 16 | 4 GB |
warp-ubuntu-latest-x64-32x | 32 | 128 GB | 32 | 4 GB |
The last column is the point. The catalog holds a constant 4 GB of RAM per vCPU at every size, so moving up the ladder buys cores and leaves the per-slot memory budget where it was. A package whose uninstrumented test binary peaks at 700 MB lands between 3.5 GB and 7 GB under the multiplier above, which overflows a 4 GB slot on any of these labels.
So size the race job in two moves. Pick the label from how wide the package graph is, then set -p from memory: -p 8 on warp-ubuntu-latest-x64-16x gives each binary 8 GB and still keeps eight compiles and test runs in flight. Repositories with a narrow package graph get nothing from the wider label and should stay on 8x with -p 4.
Raise -timeout on the same job. go test uses a default timeout of 10 minutes per test binary, and a package that finished in 90 seconds uninstrumented can cross that line once instrumented, which surfaces as a panic with a full goroutine dump rather than a test failure. -timeout 30m on the race job is the usual first setting.
The race build is a separate build cache entry
The Go build cache keys entries on the inputs that produced them, and the build flags are part of those inputs. -race therefore produces its own set of compiled package artifacts, and a race job never reuses the objects the ordinary build wrote. Three consequences follow.
First, the first race run after a cold cache pays a full compile of the dependency graph, including the standard library packages the code imports, since the Go 1.20 release notes confirm the toolchain stopped shipping precompiled standard library archives and now builds and caches the standard library the same way as any other package.
Second, plan for a GOCACHE roughly twice the size of a single-mode cache once both jobs are warm. The module cache is unaffected: GOMODCACHE holds downloaded source, which does not change with build flags, so both jobs restore the same module set.
Third, give the two jobs different cache keys, as the workflow above does with the go-race- prefix. Sharing one key makes each job save an entry that mixes its own objects with whatever the other job restored, and the entry churns on every run without either job getting a clean hit. The caching documentation covers key, version and branch scoping, and how to cache Go modules in GitHub Actions covers the module half in detail.
What the race job costs
Race jobs run longer, so the per-minute rate carries more weight than it does on the unit job. warp-ubuntu-latest-x64-16x (16 vCPU, 64 GB) costs $0.032 per minute against $0.042 per minute for the 16-core Linux larger runner (16 vCPU, 64 GB): 24 percent lower list price. GitHub list price checked on 2026-08-13 against the GitHub Actions per-minute rate list, with shapes from the GitHub runner specs page.
A worked month, with assumptions stated so you can substitute your own. Take 600 pull request runs and 120 merges to main plus nightly runs:
| Job | Label | Rate per minute | Minutes per run | Runs per month | Monthly cost |
|---|---|---|---|---|---|
| Unit | warp-ubuntu-latest-x64-4x | $0.008 | 3 | 600 | $14.40 |
| Race | warp-ubuntu-latest-x64-16x | $0.032 | 10 | 120 | $38.40 |
That is $52.80 per month. Running the race suite on all 600 pull request runs instead moves the race line to $192.00 and the total to $206.40, so gating the race job on merges and a nightly schedule is the single largest cost lever on the workflow.
Debugging a race the detector catches once
The detector reports only races that actually occur during the run, so a report from a nightly job often refuses to reproduce locally. GORACE=halt_on_error=1 stops at the first report, which keeps the log short and the failing run pinned. GORACE=history_size=N widens the per-goroutine memory access history so the report can name an older conflicting access, at the cost of more memory, which is the one setting to raise carefully on a job already near its limit.
Everything past the log is where the runner platform helps. The Action Debugger fits this case: it gives an interactive session on the machine that hit the report, with the workspace and the instrumented binary still in place. Race detection also needs a C toolchain, since the detector is built on a C runtime, so a job that sets CGO_ENABLED=0 anywhere in its environment fails to build with -race before any test runs.
Related Questions
Why does go test -race need a bigger runner than the same tests without the flag?
The race detector instruments every memory access and keeps a shadow history per goroutine. The Go documentation puts the cost for a typical program at 5x to 10x memory and 2x to 20x execution time, so a test binary that peaked at 700 MB can land between 3.5 GB and 7 GB. On WarpBuild Linux x64 runners every size carries 4 GB of RAM per vCPU, so the fix is bounding go test -p rather than only moving up the ladder. Right-sizing GitHub Actions runners walks through the same decision for other workloads.
Does the race build reuse the build cache from the normal build?
No. The Go build cache keys entries on the build inputs, and -race is one of them, so race binaries and ordinary binaries occupy separate entries. Plan for a GOCACHE roughly twice the size of a single-mode cache, and give the race job its own cache key so the two jobs stop overwriting each other. How to cache Go modules in GitHub Actions covers the key and restore-key shapes.
Can I run the race detector on ARM64, macOS and Windows runners?
Yes. The Go race detector documentation lists linux/amd64, linux/arm64, darwin/arm64 and windows/amd64 among the supported platforms. The detector is built on a C runtime, so the job needs CGO_ENABLED=1 and a working C toolchain. Go builds on WarpBuild runners covers the label choice per platform, and the pricing page lists the per-minute rate for each one.
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.