Last reviewed: 2026-08-30
Direct answer
A useful coding agent race condition testing gate asks more than whether the ordinary test suite passed. It verifies that the agent-written patch was exercised with language-appropriate race instrumentation, that the relevant concurrent behavior actually ran, and that any rare failing schedule can be replayed or reconstructed by a reviewer.
Use three layers of evidence:
- Run focused tests with a runtime race detector. For Go, that means the built-in
-racemode. For supported C and C++ builds, ThreadSanitizer provides compiler instrumentation and a runtime detector. - Exercise the instrumented program with a workload that reaches the changed shared-state paths. A clean result from a shallow unit suite says nothing about code the suite did not execute.
- Where the stack supports it, model the smallest critical concurrency state machine with deterministic schedule exploration. Loom can explore valid execution permutations for Rust code and preserve information needed to isolate a failing execution.
This combination matters because a detector result has a scope. The Go Data Race Detector documentation explicitly says that races are found only on paths executed at runtime and recommends realistic workloads when test coverage is incomplete. The result should therefore read “no race observed in this recorded workload,” not “this patch is race-free.”
Happy path: a reviewable passing run
- Record the patch commit, baseline commit, toolchain, platform, detector, test target, and workload profile.
- Build the relevant target with instrumentation enabled. Do not silently substitute an ordinary build if the instrumented build fails.
- Run the smallest test that covers the changed synchronization boundary, then run the broader package or component suite.
- Exercise realistic concurrent operations such as overlapping reads and writes, cancellation during work, repeated startup and shutdown, or competing updates to the same state.
- If a deterministic model exists, run it with a documented exploration bound and retain its completion result.
- Sanitize the report, preserve the exact exit status, and attach the command, environment-independent options, and artifact location to the pull request.
- Have the reviewer confirm that the tests reached the changed paths and that no dependency or synchronization primitive escaped instrumentation.
Choose only the commands that apply to the repository:
go test -race ./internal/cache/...
clang -fsanitize=thread -g -O1 tests/cache_race.c -o build/cache_race
./build/cache_race
RUSTFLAGS="--cfg loom" cargo test --test loom_cache --release
A passing gate requires more than a zero exit code. The instrumented target must have built, the intended test and workload must have run, all required artifacts must exist, and the evidence must identify the scope that was covered.
Error path: preserve, reproduce, then repair
- Stop the merge path when the detector reports a race, the modeled schedule fails, instrumentation is missing, or the job ends before the workload completes.
- Preserve the first complete diagnostic. For a runtime race report, retain both conflicting access stacks and the thread or goroutine creation stacks when available.
- Sanitize the output without removing file names, line numbers, test names, ordering information, or detector settings needed for reproduction.
- Rerun the same commit, toolchain, command, and workload. If the failure is intermittent, narrow the workload while preserving the conflicting operations rather than merely increasing retries.
- Add a regression test that forces or models the failing ordering. Loom’s checkpoint and trace facilities can help isolate a particular modeled execution.
- Repair the ownership, locking, channel ordering, or atomic access rule implicated by the evidence.
- Rerun the focused reproducer, the instrumented component suite, and the realistic workload. Keep both the failing and passing records in the review packet.
If the failure cannot be reproduced immediately, leave it unresolved and retain the original evidence. A one-time race report is not made harmless by a later clean run.
Who this is for
This workflow is for developers reviewing concurrency-sensitive changes produced by coding agents: caches, queues, worker pools, background refreshers, shared maps, shutdown paths, retry coordinators, and code using threads, goroutines, atomics, locks, channels, or asynchronous task state.
It is also useful for CI owners who need an auditable contract between “the detector job was green” and “the changed concurrent behavior was meaningfully exercised.” It does not require every repository to use every tool. The applicable language, build system, and concurrency surface determine the detector and test shape.
Key takeaways
- A clean race-detector result is bounded by the code paths and schedules that ran.
- Record the instrumented build separately from the ordinary test job; a fallback build invalidates the result.
- Pair focused tests with a realistic workload that reaches the changed shared state.
- Deterministic schedule exploration is more reproducible than hoping repeated stress runs encounter a rare ordering.
- Preserve failure stacks, creation stacks, exit status, toolchain, workload, and replay information.
- Treat suppressions and uninstrumented dependencies as explicit review exceptions, not invisible conveniences.
- Keep the failing record alongside the repaired passing run so the reviewer can see that the regression test detects the original defect.
Sources checked
- The Go Data Race Detector
defines a data race, documents
go test -race, explains the diagnostic stacks, and warns that only executed paths can produce findings. It also documents typical overhead of 5–10x memory and 2–20x execution time, which is relevant when setting CI timeouts. - The LLVM ThreadSanitizer documentation
explains compilation with
-fsanitize=thread, recommends debug information for useful file and line output, and describes instrumentation gaps, platform limits, suppressions, and typical overhead. It also cautions against shipping the sanitizer runtime in production executables. - The Loom concurrency-testing documentation describes deterministic exploration of concurrent executions, state reduction, replacement synchronization types, execution checkpoints, and trace output. It also explains that operations hidden behind ordinary synchronization types are not modeled and that large state spaces may require bounded exploration.
Together, these sources support a layered workflow: runtime detection for exercised code, intentional workload coverage, and deterministic modeling for small critical state machines. They do not support claiming that any single clean run proves the absence of every concurrency defect.
Contract details to verify
Instrumented-build contract
Record the detector and the exact build path. A Go job should show that -race was applied to the tests or binary that actually ran. A ThreadSanitizer job should show that the applicable code was compiled and linked with -fsanitize=thread; precompiled or ignored modules must be listed because incomplete instrumentation can miss races or distort reports. A Loom test should identify which synchronization types are modeled and which dependencies remain outside the model.
The job should fail closed when instrumentation cannot be enabled. Unsupported platforms, missing compiler prerequisites, link failures, or incompatible libraries are coverage failures—not clean race results.
Workload contract
Name the shared resource and the operations expected to overlap. Record the test target, concurrency level, iteration or exploration bound, duration, cancellation behavior, and completion marker. For an agent change to a cache, for example, the workload might overlap lookup, refresh, eviction, and shutdown while asserting invariants about visibility and ownership.
Run the same workload against the baseline when practical. This distinguishes a patch-introduced report from a known pre-existing report without excusing either one. Keep the baseline and patch results tied to their commits.
Sanitized evidence contract
A compact machine-readable record makes the result comparable across reruns. Keep identifiers short and operational, and omit environment dumps, request bodies, process tables, or unrelated configuration. Those can contain sensitive material without helping a reviewer understand the race.
{
"run_id": "race-ci-482",
"commit": "8c12e4a",
"toolchain": "go-1.25",
"detector": "go-race",
"test_target": "./internal/cache",
"workload_profile": "parallel-cache-refresh",
"result": "failed",
"exit_code": 66,
"race_count": 1,
"first_conflict": "internal/cache/store.go:87",
"artifact_path": "artifacts/race/report-482.txt",
"started_at": "2026-08-30T10:20:00Z",
"duration_ms": 84231,
"redaction_status": "complete"
}
The detailed artifact should retain the detector’s conflict stacks, thread-creation context, and relevant test output. Sanitize values before upload, but do not rewrite technical evidence into a summary that another operator cannot replay.
For a consistent command trail, use the practices in Keep Terminal Command Evidence Reviewable in Coding Agent Runs . Keep the code change itself small enough to correlate with the report by following How to Produce Reviewable Diffs From Coding Agent Sessions .
Acceptance contract
Define the decision before the job runs. A passing result should require a successful instrumented build, completed targeted tests, completed workload, zero detector findings, no unexplained exclusions, and a present evidence artifact. If deterministic exploration is used, record whether the configured state space completed or stopped at a bound.
Route the run to the error path when any detector finding appears, the test fails, the detector process exits unexpectedly, the workload completion marker is missing, or instrumentation coverage cannot be established. A timeout is not a passing result. Increase resources, reduce the model, or narrow the workload while retaining the behavior under review.
Failure modes
- Green ordinary tests mistaken for race testing. The suite passes, but no instrumented binary ran. Make the detector identity and build flags mandatory evidence fields.
- A clean detector run over the wrong path. Runtime detectors cannot report a race in code that never executes. Require a workload-to-change map showing which changed shared-state operations ran.
- Mixed instrumented and uninstrumented code. ThreadSanitizer generally needs applicable code compiled with its instrumentation. Uninstrumented libraries can create missed findings or misleading reports, so list them explicitly.
- Incomplete Loom substitution. Loom cannot reason about concurrent operations hidden behind ordinary synchronization types. Review the modeled type boundary and dependencies before accepting exploration results.
- Random stress without replay data. A loop eventually fails, but the operator retains neither the triggering order nor enough workload context to recreate it. Preserve the earliest full report and add a forced or modeled regression case.
- Sanitizer overhead treated as an application regression. Go’s detector and ThreadSanitizer can substantially increase runtime and memory use. Give the detector job its own measured budget while still treating an incomplete timeout as an error.
- Suppression drift. A broad suppression hides new reports after ownership or code paths change. Require a narrow pattern, an owner, a reason, and a removal condition; show suppressions in the evidence packet.
- Sleep-based repair. Adding a delay may make one schedule less frequent without establishing synchronization. Require a regression test that demonstrates the ordering rule, then verify the repaired code under instrumentation.
- Instrumented test binary promoted to production. ThreadSanitizer’s documentation says its runtime is a testing tool and is not intended for production executables. Keep test and release artifacts separate.
- Logs that cannot be shared safely. Full environment or payload capture increases exposure without improving concurrency analysis. Retain only the sanitized fields and technical stacks needed to reproduce the failure.
FAQ
Is a clean race-detector run proof that the patch is safe?
No. It is evidence that the detector observed no report in the instrumented code paths and schedules exercised by that run. The review should state the test target, workload, duration or bound, and known coverage gaps.
Which tool should a coding-agent patch use?
Use the tool that matches the repository. Go provides -race for supported builds. Clang’s ThreadSanitizer instruments supported C and C++ targets. Loom is a Rust modeling tool for deliberately bounded concurrent components and requires its replacement types. Runtime detectors and deterministic models answer related but different questions, so a critical component may benefit from both applicable layers.
Should the detector job run on every pull request?
A practical policy is to run focused detector tests on every concurrency-sensitive patch and schedule broader instrumented workloads separately when their overhead is too high for the main gate. The policy must not silently skip the focused job. Label an omitted run as missing evidence and require the reviewer to resolve it.
What should happen when only one run reports a race?
Preserve that run and treat it as unresolved. Capture both conflicting access stacks, creation stacks when available, commit, toolchain, command, workload, and exit status. Then reduce the workload or model the ordering. Repeated clean runs do not erase the original diagnostic.
Can a known third-party race be suppressed?
ThreadSanitizer supports suppressions, but a suppression changes what the detector can report. Keep it narrow, document the affected module and evidence, assign an owner, and define when it will be removed. Do not use a repository-wide pattern merely to restore a green job.
What belongs in the pull request?
Include the detector command, instrumented build result, workload description, sanitized structured record, full diagnostic artifact when a failure occurred, regression test, baseline comparison when available, and the final passing rerun. A reviewer should be able to connect the changed lines to the conflicting operations without recreating the entire agent session.
How should a large Loom model be handled?
Reduce it to the smallest shared-state contract first. Mock unrelated nondeterminism, keep modeled operations behind Loom-aware types, and record any exploration bound. If the state space does not complete, report that limitation rather than presenting the run as exhaustive.
Reader next step
Choose the most synchronization-sensitive file in the agent patch and name the shared resource it changes. Add one language-appropriate detector job, one focused overlapping-operation test, and one structured evidence record using the fields above. Run the same target on the baseline and patched commits.
Before requesting review, confirm that the instrumented build ran, the workload reached the changed path, the artifact is sanitized, and any failure can be replayed or reconstructed. Attach both the original failure and the repaired passing run when applicable. That gives the reviewer a concrete concurrency claim to verify instead of a generic green check.