When does synctest.Wait return, and which blocked goroutines count as durably blocked?
answer
- it waits on the other goroutines, not the clock
- blocked is not the same as durably blocked
- only a bubble-mate may unblock it
- a real socket read never qualifies
basics
~20 ssynctest.Wait returns once every other goroutine in the caller's bubble is durably blocked: blocked so that only another goroutine in that same bubble could unblock it, such as a bubble-created channel operation, time.Sleep, WaitGroup.Wait or Cond.Wait.
solid answer
~50 s`synctest.Wait` blocks the calling goroutine until every other goroutine in its bubble is *durably* blocked — blocked in a way only another bubbled goroutine can undo. That covers a send or receive on a channel created inside the bubble, a `select` over such channels, `time.Sleep`, `sync.WaitGroup.Wait`, `sync.Cond.Wait` and a nested `synctest.Wait`. It does not cover blocking on a system call, on real network or file I/O, or on a channel created outside the bubble, because something outside could unblock those at any moment. The practical use is to replace "sleep a bit and hope": after nudging a background sweeper, call `Wait` and you know it has run as far as it can before you assert. If a goroutine is stuck on something non-durable, `Wait` simply never returns, and that is your signal the dependency is not inside the bubble.
code
go · 12 linessynctest.Test(t, func(t *testing.T) {
c := newCache(time.Minute)
refreshed := make(chan string, 4) // created inside the bubble
go c.refreshEvery(30*time.Second, refreshed)
time.Sleep(90 * time.Second)
synctest.Wait() // the refresher is parked on its ticker again
if got := len(refreshed); got != 3 {
t.Fatalf("refreshes = %d, want 3", got)
}
})go deeper
Remember the shape rather than the fine print: do the thing, call synctest.Wait, then assert. It is what you write instead of a small sleep when a background goroutine has to catch up.
State the condition exactly — every other goroutine in the bubble is blocked such that only a bubble-mate could unblock it — and give two examples on each side, such as a bubble-created channel receive versus a socket read.
Treat a Wait that never returns as evidence. Name what is not bubbled — real I/O, a channel created outside, a spinning loop — and fix the dependency rather than adding a timeout around the wait.
Own the convention: if the suite is going to rely on settling rather than sleeping, the code under test must take its collaborators in a form that can live inside a bubble, and that constraint belongs in review standards, not in individual tests.
## The question Wait answers Asserting on state that a background goroutine updates is the classic flaky-test shape. The test starts a sweeper, asserts immediately, and loses the race. The usual patch is `time.Sleep(10 * time.Millisecond)` — which is not a synchronisation primitive, just a bet on scheduling. `synctest.Wait` replaces the bet with a stated condition: *stop here until nobody else in this bubble can make progress on their own*. ## The definition, precisely A goroutine in a bubble is **durably blocked** when it is blocked and the only thing that could unblock it is another goroutine in the same bubble. That distinction, not the mere fact of being blocked, is what the runtime tracks. Operations that block durably inside a bubble include: - a send or receive on a channel created inside the bubble; - a `select` whose cases are all such channels (a nil channel included, since nothing can ever unblock it); - `time.Sleep`; - `sync.WaitGroup.Wait`, for a group used within the bubble; - `sync.Cond.Wait`; - `synctest.Wait` itself. Operations that do **not** block durably include: - reading or writing a real network connection, a file, or anything else that ends in a system call; - receiving from or sending to a channel that was created outside the bubble, because a goroutine outside can complete it at any moment; - spinning in a loop — that goroutine is not blocked at all, it is running. ## Why the distinction exists The bubble's fake clock is only allowed to jump forward when doing so cannot skip real work. If every goroutine inside is durably blocked, nothing inside can change anything, so advancing to the next timer deadline is safe and instant. If a goroutine might be about to be woken by the outside world, the bubble cannot know that, so it does not advance. `Wait` and the clock share one notion of "settled" for exactly this reason. ## How you use it The pattern is: perform the stimulus, `synctest.Wait()`, then assert. ```go synctest.Test(t, func(t *testing.T) { c := newCache(time.Minute) go c.sweep(ctx) // background sweeper, ticking every 30s c.Set("k", "v") time.Sleep(90 * time.Second) // virtual synctest.Wait() // the sweeper has run as far as it can if _, ok := c.Get("k"); ok { t.Fatal("swept entry still present") } }) ``` Without the `Wait`, the sweeper may be runnable but not yet run when the assertion executes. With it, the assertion runs at a defined point: the sweeper is parked back on its ticker, having done everything the elapsed virtual time entitled it to do. Note what `Wait` is not. It is not a barrier the other goroutines participate in — they never call anything. It is not a pause: after it returns, the other goroutines are blocked, but the moment the caller unblocks one of them they run again. And it does not by itself finish anything; a goroutine parked on a ticker is durably blocked and will stay that way until the clock moves. ## When Wait hangs A `Wait` that never returns is a diagnosis, not a mystery. Something in the bubble is blocked non-durably: a real HTTP call, a database driver's socket, a channel the test created before entering the bubble, or a goroutine spinning rather than blocking. The fix is to bring that dependency inside the bubble — an in-memory implementation, or a channel created within the test function — rather than to add a timeout around the wait. ## Related shapes A test often wants "advance virtual time, then settle": `time.Sleep(d)` followed by `synctest.Wait()`. Go 1.27 added `synctest.Sleep` for that pairing. On Go 1.25 and 1.26 the two calls in sequence are the idiom. Calling `synctest.Wait` outside any bubble is a programming error and panics; it only means something relative to a bubble. ## What an interviewer is checking That you can state the condition — *every other goroutine in this bubble is durably blocked* — rather than describing `Wait` vaguely as "waits for the goroutines to finish". Finishing is a different rule, enforced at the end of the bubble; `Wait` is about settling, and a settled goroutine is usually still alive.
- Is a goroutine blocked receiving on a channel created before the bubble started durably blocked?No. A goroutine outside the bubble still holds that channel and could send at any moment, so the block is not durable and `synctest.Wait` will never return. Create channels the bubbled code waits on inside the test function.
- Does synctest.Wait guarantee the other goroutines have finished their work?It guarantees they cannot make further progress unaided — usually because they are parked on a ticker, a channel or a sleep. They are typically still alive. Requiring them to have exited is a separate rule, enforced when the bubble's function returns.
- What happens if synctest.Wait is called outside a bubble?It panics. `Wait` is defined relative to the caller's bubble, so there is nothing sensible to wait for outside one. In practice this shows up when a helper is shared between bubbled and unbubbled tests.
Wait asks "is everyone else stuck waiting on each other?", not "has enough time gone by?" — the difference between a condition and a guess.
saying these in an interview costs you the question
- Describes Wait as waiting for the other goroutines to exit
- Thinks Wait advances the clock by a fixed amount
- Counts a blocking network read as durably blocked
- Believes a spinning goroutine will let Wait return
- Wraps Wait in a timeout instead of fixing the outside dependency