Session

When Tests Become Slot Machines: Deterministic Concurrency with synctest

Your concurrent test passed 47 times. Then it failed. You ran it again. It passed. Congratulations, you now have a slot machine.

Testing concurrent Go code has always meant choosing between slow (sprinkle time.Sleep everywhere) and flaky (accept random failures) or something like a "broken clock" abstraction. Go 1.25's synctest eliminates this choice entirely. Tests that took 20 minutes now run in the blink of an eye. Tests that failed randomly now pass deterministically. Every time!

This talk shows how synctest creates isolated "bubbles" where time is fake, goroutines can be observed at rest, and "this should not have happened" becomes testable.


Main idea: The 'testing/synctest' package creates isolated "bubbles" where time is fake and goroutines can be observed at rest. Inside a bubble, 'time.Sleep(1*time.Hour)' completes in microseconds, and 'synctest.Wait()' returns only when all goroutines are "durably blocked", waiting on channels, timers, or mutexes with no way to proceed without external input.

Questions this talk answers:

- Why can't time.Sleep prove a negative? To test "nothing happens for 5 seconds," you'd sleep 5 real seconds. But that doesn't prove nothing happened, it proves nothing happened *yet*. And if you sleep too short, you get flaky failures. synctest solves this: fake time advances instantly, and 'Wait()' tells you when all goroutines have stabilized.

What is "durably blocked"? A goroutine is durably blocked when it's waiting on something that won't resolve without external action: a channel receive with no sender ready, a timer that hasn't fired, a mutex held by another blocked goroutine. 'synctest.Wait()' returns when ALL bubble goroutines reach this state. This is deterministic—no timing assumptions needed.

How does fake time work? Inside a bubble, 'time.Now()', 'time.Sleep()', 'time.After()', and 'time.Timer' all use simulated time. The runtime advances this clock when goroutines are durably blocked on timers. A test can "sleep" for hours without burning wall-clock time.

What are the limitations? Real I/O (network, disk) involves goroutines outside the bubble—they won't be tracked. Subtests create separate bubbles. Global goroutines (started before the bubble) aren't isolated.

Attendees will know how to convert flaky tests to deterministic ones (with before/after examples), understand the "durably blocked" concept that makes it work, and recognize when synctest won't help. Tests that took 20 minutes with sleep-based synchronization now run in milliseconds.

Beginner-level adaptation:

For a beginner audience, the talk would focus on the problem (flaky tests) and the solution (replace time.Sleep with
synctest.Wait) without diving into runtime mechanics. I'd start by showing a real flaky test that passes 9 times out of10, then fails mysteriously in CI—a pain point every Go developer recognizes.

The core message: time.Sleep(100*time.Millisecond) is a guess, synctest.Wait() is a guarantee. Code examples would show simple before/after transformations: replace sleep with Wait, wrap the test in synctest.Run, done. The talk would cover the three things to remember: time is fake inside the bubble, Wait returns when goroutines have nothing to do, and real I/O breaks the isolation. Attendees would leave able to convert their existing flaky concurrent tests to deterministic ones using a mechanical pattern.

Advanced-level adaptation:

For an advanced audience, the talk would explore how synctest's bubble abstraction is implemented in the runtime and its edge cases. I'd examine how the runtime tracks goroutines belonging to a bubble, how fake time is implemented (the faketime clock, timer heap manipulation), and the precise definition of "durably blocked", blocked on a channel with no ready sender/receiver, a mutex held by another durably blocked goroutine, or a timer in fake time.

The talk would cover subtle failure modes: why a for { select { default: } } loop hangs Wait forever (never durably blocked), howruntime.Gosched() differs from blocking, and why goroutines started before the bubble aren't tracked. I'd also examine testing strategies for code that mixes real I/O with timers, how to structure tests when bubble isolation isn't enough, and the implementation differences between synctest.Run and synctest.Test. Attendees would leave understanding the runtime machinery well enough to debug when Wait hangs unexpectedly and design testable concurrent APIs from the start.

Alex Rios

Principal Engineer @Memed

Curitiba, Brazil

Actions

Please note that Sessionize is not responsible for the accuracy or validity of the data provided by speakers. If you suspect this profile to be fake or spam, please let us know.

Jump to top