Sleep for Synchronization — Specification¶
Table of Contents¶
- Purpose
- Scope
time.SleepSemantics- The Monotonic Clock
- The Wall Clock
- Timer Specifications
- Ticker Specifications
time.AfterAndtime.AfterFunc- Scheduler Implications
testing/synctestSpecification- Race Detector And Sleep
- Happens-Before Edges Around Sleep And Timers
- Context And Timer Interaction
- Platform Differences
- Allocation Behaviour
- Sleep In Locked Threads
- Sleep In Cgo
- Sleep In Signal Handlers
- References To The Go Specification
- Quick Reference
Purpose¶
This document is the normative reference for time.Sleep and related timing primitives in Go, as relevant to the "sleep for synchronisation" anti-pattern. It does not attempt to specify the Go language or runtime; rather it states precisely what the standard library guarantees and what it does not, so that callers can reason about correctness.
Scope¶
Covered:
time.Sleep.time.Now.time.NewTimer,time.Timer.Stop,time.Timer.Reset.time.NewTicker,time.Ticker.Stop,time.Ticker.Reset.time.After,time.AfterFunc,time.Tick.testing/synctest.Test,testing/synctest.Wait.- Interaction with
context.Contextdeadlines and cancellation. - Interaction with the Go memory model.
Not covered:
- The full Go language specification.
- The internal goroutine scheduler beyond what callers can observe.
- Operating system kernel timer mechanisms.
time.Sleep Semantics¶
Stated guarantees¶
- Pause duration.
time.Sleep(d)pauses the current goroutine for at leastd. - Negative or zero durations.
time.Sleep(0)andtime.Sleep(d)withd <= 0return immediately without parking the goroutine. - Caller resumes. After
time.Sleepreturns, control returns to the caller.
Not guaranteed¶
- Upper bound on actual duration. There is no upper bound: the goroutine may wake at
t + d + ε, whereεis non-negative and unbounded.εdepends on scheduler load, GC activity, OS scheduling, etc. - Order of wakeups. If multiple goroutines
Sleep(d)for the samed, the order in which they resume is unspecified. - Cancellation.
time.Sleephas no cancellation channel. The goroutine sleeps the full duration regardless of context, signals, or other goroutines. - Interaction with signals. Go's signal handling does not interrupt
time.Sleep.
Implementation notes (non-normative)¶
The Go runtime implements time.Sleep by creating a timer with when = nanotime() + d and parking the goroutine via gopark. The timer fires when the runtime's scheduler reaches it, at which point the goroutine is unparked and placed on the local run queue.
The Monotonic Clock¶
Specification¶
Since Go 1.9, time.Now() returns a Time value that contains both:
- A wall clock reading (
time.Time.Wall). - A monotonic clock reading (
time.Time.ext).
Operations on time.Time use the monotonic component when both operands carry it. Specifically:
t1.Sub(t2)returns(t1.mono - t2.mono)if both have monotonic readings; otherwise uses wall clock.t1.Before(t2),t1.After(t2),t1.Equal(t2)use monotonic clock when both have it.time.Since(t)is equivalent totime.Now().Sub(t).time.Until(t)is equivalent tot.Sub(time.Now()).
Stripping the monotonic component¶
The monotonic component is stripped by:
t.Round(0)(a no-op for the wall clock, but explicitly drops monotonic).- Marshalling (
MarshalBinary,MarshalText,MarshalJSON). t.Truncate(d),t.Round(d)ford > 0.t.In(loc).
After stripping, comparisons and subtraction use the wall clock and may be affected by NTP adjustments or system clock changes.
Guarantees¶
- Monotonic clock never goes backwards within a single process.
time.Since(start)is always non-negative ifstartretains its monotonic reading.time.Sleep(d)is implemented against the monotonic clock; wall clock changes do not affect it.
Non-guarantees¶
- Monotonic clock readings are not comparable across processes.
- Monotonic clock has no defined epoch; the value is meaningful only as a delta within one process.
The Wall Clock¶
Specification¶
The wall clock component of time.Time represents calendar time. It is subject to:
- NTP corrections (may jump forward or backward).
- System clock changes (
settimeofday). - Timezone changes (DST).
- Leap seconds (Linux: may step or smear depending on kernel/NTP config).
Use cases¶
- Display to humans.
- Persistence (databases, logs).
- Calendar-based scheduling (e.g. "run at 03:00 every day").
Anti-uses¶
- Measuring elapsed time within a process: use monotonic clock (
time.Since). - Deciding "did N seconds pass since X happened?" within a process: use monotonic.
Timer Specifications¶
type Timer struct {
C <-chan Time
// ...
}
func NewTimer(d Duration) *Timer
func (t *Timer) Stop() bool
func (t *Timer) Reset(d Duration) bool
NewTimer(d)¶
- Creates a
Timerthat sends the current time ont.Cafter at leastd. - The channel
t.Cis buffered with capacity 1. - If no goroutine receives from
t.Cbefore the timer fires again (afterReset), the buffered value remains; the runtime does not block while trying to send.
Stop() semantics¶
- Returns
trueif the call stops the timer before it fires. - Returns
falseif the timer has already fired or been stopped. - Does not drain
t.C. If the timer fired but the channel was not yet received, the value remains.
Standard idiom (pre-Go 1.23):
Go 1.23 changed timer semantics: after Stop, the channel is implicitly drained, so the boilerplate is no longer required. Code targeting older Go must still drain manually.
Reset(d) semantics¶
- Changes the timer to fire after at least
dfrom now. - Returns
trueif the timer was active,falseif expired or stopped. - Pre-Go 1.23: must call
Stopfirst; otherwise old fires may interleave with new ones. - Go 1.23+:
Resethandles draining correctly without explicitStop.
t.C behaviour¶
- Single buffered channel.
- Receives the time the timer fired (which may differ from the requested fire time by an unbounded
ε). - After
Stop, no further sends occur (pre-1.23: unless the timer had already started firing; post-1.23: cleanly).
Ticker Specifications¶
type Ticker struct {
C <-chan Time
}
func NewTicker(d Duration) *Ticker
func (t *Ticker) Stop()
func (t *Ticker) Reset(d Duration)
NewTicker(d)¶
- Creates a
Tickerthat sends the current time ont.Ceveryd. dmust be positive; panics otherwise.- The channel
t.Cis buffered with capacity 1.
Drift accumulation¶
- Ticks do not accumulate. If the receiver is slow, ticks are dropped (the channel buffer is 1).
- The ticker schedules each tick as
previous + d, not asnow + d. This means missed ticks do not cause future ticks to drift. - However, if
dis so small that the runtime cannot keep up, ticks are effectively rate-limited.
Stop()¶
- Stops the ticker. No further sends occur.
- Does not close
t.Cor drain the buffered value.
Reset(d) (Go 1.15+)¶
- Changes the tick period to
d. - The first tick after
Resetis atnow + d.
time.Tick(d)¶
- Convenience function returning
NewTicker(d).C. - The underlying
*Tickeris not exposed and cannot be stopped. Leaks for the lifetime of the program. - Acceptable only for one-shot top-level program control where the leak is the same as program exit.
time.After And time.AfterFunc¶
time.After(d Duration) <-chan Time¶
- Equivalent to
time.NewTimer(d).C. - The underlying timer cannot be stopped.
- Each call allocates a new timer.
- For repeated use in a loop, prefer
time.NewTimer+Resetto avoid allocation churn. - Pre-Go 1.23: leaks if
selectchooses another case. Post-1.23: garbage-collected normally.
time.AfterFunc(d Duration, f func()) *Timer¶
- Schedules
fto run after at leastd. fruns on a dedicated goroutine (not the caller's).- Returns a
*Timerthat can beStopped orReset. - If
Stopreturnsfalse,fmay have already started or completed.
f callback semantics¶
fshould be short and non-blocking. Long callbacks delay other timers on the same scheduling lane.fmay run concurrently with other goroutines; synchronise shared state as usual.- Panics in
fcrash the program just like any other goroutine panic.
Scheduler Implications¶
Goroutine state during Sleep¶
A goroutine in time.Sleep:
- Is in state
Gwaitingwith reasonwaitReasonSleep. - Holds no CPU.
- May be moved between Ps by the runtime.
Wakeup mechanism¶
When the timer fires:
- The runtime calls
goreadyon the goroutine. - The goroutine is placed on the firing P's local run queue.
- The goroutine is scheduled when the P is free.
Wake latency¶
Wake latency is the time from "timer fire time" to "goroutine actually running". It is:
- Sub-microsecond under no contention.
- Bounded by
GOMAXPROCSand the runtime's preemption rate (10ms by default since Go 1.14). - Effectively unbounded under heavy load.
GOMAXPROCS interaction¶
With GOMAXPROCS=1, all sleeping goroutines wake serially. The first to wake may run for up to the preemption quantum (10ms) before the second runs. This is a common cause of test flakiness in CI runners that pin to 1 CPU.
Preemption¶
Go 1.14+ supports asynchronous preemption. A goroutine running too long is interrupted by a signal and rescheduled. time.Sleep itself is not affected (it parks immediately), but the goroutine you are racing against may be preempted between operations, changing the effective timing of side effects.
testing/synctest Specification¶
The testing/synctest package (Go 1.24+) provides deterministic time control for tests.
synctest.Test(t *testing.T, f func(t *testing.T))¶
- Runs
fin a bubble: a goroutine group with a virtual clock. - All goroutines spawned inside
f(transitively) are members of the bubble. - Time-related calls inside the bubble use the virtual clock:
time.Now()returns virtual time.time.Sleep(d)parks until virtual clock advances byd.time.NewTimer,time.After,time.AfterFunc,time.NewTickeruse virtual time.- The virtual clock starts at midnight UTC on a fixed date (currently
2000-01-01). - When all goroutines in the bubble are durably blocked, the virtual clock advances to the next pending timer.
- When
freturns, the bubble exits and any remaining goroutines are reported as leaks.
synctest.Wait()¶
- Inside a bubble, blocks the calling goroutine until all other bubble goroutines are durably blocked.
- Does not advance virtual time.
- Returns when the bubble reaches a quiescent state.
- If called outside a bubble, panics.
Durably blocked¶
A goroutine is durably blocked if it is parked on an operation that can only be unblocked by:
- Other bubble goroutines.
- The bubble's virtual clock.
Operations that satisfy this:
- Channel send/receive on a bubble-created channel.
- Mutex acquire on a mutex used only inside the bubble.
time.Sleep,time.After, etc.sync.Cond.Wait.runtime.Gosched(treated as a yield, not durable block — does not contribute to advancement).
Operations that do not satisfy:
- File I/O.
- Network I/O.
syscallcalls.- Cgo calls.
- Channel operations on channels created outside the bubble.
Deadlock detection¶
If all bubble goroutines are durably blocked and no pending timer can fire (the timer heap is empty), synctest.Test panics with a deadlock message. This is the diagnostic for "missing producer" bugs.
Output guarantees¶
time.Since(t)returns the virtual elapsed time sincetwas captured inside the bubble.- Cross-bubble time comparisons are unspecified.
- The bubble's virtual time is not synchronised with the OS clock or with other bubbles.
Race Detector And Sleep¶
What -race detects¶
- Concurrent unsynchronised access to a memory location where at least one access is a write.
- Implemented via the LLVM ThreadSanitizer algorithm with happens-before tracking.
What -race does not detect related to sleep¶
- Insufficient sleep duration (the "sleep too short" flake). The test reads valid memory; the read just happens before the producer wrote, but that is not a race in the technical sense — there is no concurrent unsynchronised access if the read happens before the write at all.
- Goroutines outliving the test.
- Time-ordering bugs unrelated to memory.
Implication¶
A test that passes -race and contains time.Sleep may still be flaky. The race detector is necessary but not sufficient.
Happens-Before Edges Around Sleep And Timers¶
The Go memory model (https://go.dev/ref/mem) specifies happens-before relations. Relevant edges:
time.Sleep¶
- The call to
time.Sleep(d)and the return from it are sequenced within the same goroutine. time.Sleepdoes not establish a happens-before edge with operations in other goroutines.
Timer fire¶
- The send of
time.Timeont.Chappens before the receive completes. - The receive on
t.Chappens before subsequent operations in the receiving goroutine.
time.AfterFunc¶
- The call to
AfterFuncis synchronised with the start of the callbackfvia the channel/timer machinery; specifically, all writes before theAfterFunccall happen beforefbegins executing.
sync.WaitGroup.Wait¶
wg.Done()happens beforewg.Wait()returns. This is why waitgroups synchronise; sleeps do not.
Channel close¶
- A close of a channel happens before any receive on that channel observes the close.
synctest.Wait¶
- All operations performed by other bubble goroutines before they durably blocked happen before
synctest.Wait()returns.
Context And Timer Interaction¶
context.WithTimeout(parent, d)¶
- Creates a derived context.
ctx.Done()is closed afterd(using the runtime's timer machinery) or whenparent.Done()closes, whichever is first.ctx.Err()returnscontext.DeadlineExceededif the timeout fired.
context.WithDeadline(parent, t)¶
- Same as
WithTimeoutbut with an absolute deadline.
context.AfterFunc(ctx, f) (Go 1.21+)¶
- Calls
fwhenctx.Done()closes. - Returns a stop function; calling it removes the callback.
fruns on a new goroutine.
Cancellable wait pattern¶
This is the canonical cancellable sleep. time.After cannot be Stopped; in Go 1.23+ it is GC'd cleanly when no longer referenced, in older Go versions it leaks the underlying timer until it fires.
For repeated waits, use time.NewTimer + Stop to avoid the leak.
Platform Differences¶
Linux¶
- Monotonic clock:
CLOCK_MONOTONICvia vDSO when available. - Wall clock:
CLOCK_REALTIMEvia vDSO when available. - Timer resolution: 1ms typical, 100µs with tickless kernels.
macOS / Darwin¶
- Monotonic clock:
mach_absolute_time(no syscall). - Wall clock:
gettimeofdayorclock_gettime. - Timer resolution: ~1ms.
Windows¶
- Monotonic clock:
QueryPerformanceCounter. - Wall clock:
GetSystemTimeAsFileTime. - Timer resolution: 16ms by default; can be improved with
timeBeginPeriod(1)to 1ms.
Implication for tests¶
A test that relies on sub-millisecond timing accuracy will behave differently across platforms. Use synctest or a fake clock for deterministic timing.
Allocation Behaviour¶
time.Sleep¶
- Allocates a
*runtime.timerper call (Go runtime detail; not exposed to user). - The allocation is small (~80 bytes).
- The runtime pools timers per-P to reduce allocation pressure.
time.After¶
- Allocates a new
Timerper call. The*Timerincludes a channel. - Cost: ~100-150ns per call plus the channel allocation.
time.NewTimer + Reset¶
- One allocation at creation;
Resetreuses. - Preferred in hot loops.
time.NewTicker¶
- One allocation at creation.
- Internal state is reused; only
Stopreleases.
Garbage collection¶
- Stopped timers are GC'd normally once unreferenced.
- Pre-Go 1.23: a
time.Afterwhose channel is unreferenced was not GC'd until the timer fired. This was a leak in long-running selects. - Go 1.23+: garbage collector can reclaim unfired timers whose channels are unreferenced.
Sleep In Locked Threads¶
runtime.LockOSThread¶
Pins the calling goroutine to its current OS thread. The thread is dedicated to that goroutine; no other goroutine can run on it.
Sleep on a locked thread¶
- The goroutine is parked normally.
- The OS thread is idle (not running other goroutines).
- Other goroutines pinned to other threads run normally.
- Other unpinned goroutines run on other Ps' threads.
Implication¶
If many goroutines are LockOSThread-ed and all are sleeping, the corresponding OS threads are wasted. In CGo-heavy programs (e.g. OpenGL contexts pinned to threads), this can starve the program.
Avoid time.Sleep in LockOSThread goroutines. Use channels or condition variables instead.
Sleep In Cgo¶
Cgo call semantics¶
A goroutine in a C.foo() call is in state Gsyscall. The runtime detects long syscalls and may spawn additional OS threads (runtime.lockedm).
time.Sleep after Cgo¶
After returning from Cgo, time.Sleep works normally on the Go side. The OS thread used for the Cgo call may be released back to the runtime pool.
time.Sleep inside Cgo (C code)¶
If C code calls sleep(3) or nanosleep, the OS thread is blocked from the runtime's perspective. Go has no virtual-time control over C-level sleeps; testing/synctest cannot fake them.
Implication: do not test code that includes C-level sleeps with synctest. Stub the C function for testing.
Sleep In Signal Handlers¶
Go signal handlers¶
Go signal handlers run on a special goroutine (the "signal goroutine") that the runtime spawns. User-installed handlers (via signal.Notify) receive signals on a channel; the handler itself is just a channel receive.
Sleep in a signal goroutine¶
The signal goroutine should not sleep. If it does, subsequent signals queue up and may be coalesced; signal delivery is delayed.
Recommendation¶
Signal handlers should be short:
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
go func() {
for sig := range sigCh {
log.Printf("got signal %v", sig)
// do not sleep here
}
}()
References To The Go Specification¶
The Go specification does not directly cover time.Sleep (it is a library function, not a language primitive). The relevant references:
- The Go Programming Language Specification: https://go.dev/ref/spec — primarily for memory model, goroutine semantics, channels.
- The Go Memory Model: https://go.dev/ref/mem — happens-before rules for goroutines, channels, mutexes, atomics, and finalisers.
timepackage documentation: https://pkg.go.dev/timetesting/synctestpackage documentation: https://pkg.go.dev/testing/synctestcontextpackage documentation: https://pkg.go.dev/context
Quick Reference¶
time.Sleep(d) invariants¶
- Sleeps for at least
d; upper bound unspecified. - Returns immediately for
d <= 0. - Not cancellable.
- Uses monotonic clock.
- No happens-before with other goroutines.
Timer invariants¶
t.Cis buffered (cap 1).Stopdoes not drain.Resetsemantics differ before/after Go 1.23.
Ticker invariants¶
t.Cis buffered (cap 1).- Ticks are dropped, not queued.
Stopmust be called to avoid leaks.
time.After invariants¶
- Allocates per call.
- Timer GC behaviour improved in Go 1.23.
synctest invariants¶
- Virtual time advances only when all bubble goroutines durably blocked.
- External I/O breaks the bubble.
synctest.Waitis a quiescence barrier, not a time advancer.
Happens-before¶
wg.Done→wg.Waitreturn: yes.close(ch)→<-ch: yes.time.Sleep→ other goroutine's operations: no.synctest.Wait→ other bubble goroutines' prior operations: yes.
Cancellable wait¶
Repeated wait without leak¶
t := time.NewTimer(d)
defer t.Stop()
for {
select {
case <-t.C:
// do work
t.Reset(d) // Go 1.23+: safe; older Go: must drain first
case <-ctx.Done():
return
}
}
time.Tick warning¶
Replace with NewTicker + Stop for any non-trivial use.