time.Sleep(2 * time.Second) pauses the calling goroutine. It takes a time.Duration, not a number — time.Sleep(2) compiles and sleeps for two nanoseconds, which is the single most common mistake with it. And it cannot be cancelled: once you are in a Sleep, a context cancellation, a SIGTERM, or a completed job cannot wake you early. For anything that might need to stop, use select with ctx.Done().
Sleep is fine in the narrow cases and wrong in most of the ones people reach for it in, so this is mostly about which is which.
Table of contents
- Durations are typed, and that is the trap
- Sleep cannot be interrupted
- Ticker for periodic work
- Retry with backoff
- Where Sleep in tests goes wrong
- When Sleep is genuinely fine
- How this fits the rest of the stack
- FAQ
Durations are typed, and that is the trap
import "time"
time.Sleep(2 * time.Second)
time.Sleep(500 * time.Millisecond)
time.Sleep(time.Minute)
// Compiles. Sleeps for 2 nanoseconds.
time.Sleep(2)
// From a variable -- needs an explicit conversion
seconds := 5
time.Sleep(time.Duration(seconds) * time.Second)
time.Duration is an int64 count of nanoseconds. An untyped constant like 2 converts to it silently, which is why time.Sleep(2) is legal and useless. The compiler cannot help because the code is valid.
The conversion from an int variable trips people too: time.Duration(seconds) * time.Second is right, time.Duration(seconds * time.Second) does not compile, and seconds * time.Second does not either. Convert the number, then multiply by the unit.
Negative or zero durations return immediately rather than erroring, so a computed delay that goes negative silently becomes a busy loop.
Sleep cannot be interrupted
This is the reason most time.Sleep calls in production code are wrong.
// Bad: shutdown waits up to 30 seconds for this to finish
func worker(ctx context.Context) {
for {
doWork()
time.Sleep(30 * time.Second) // ignores ctx entirely
}
}
// Good: wakes immediately on cancellation
func worker(ctx context.Context) {
for {
doWork()
select {
case <-ctx.Done():
return
case <-time.After(30 * time.Second):
}
}
}
With Sleep, a SIGTERM during the pause means your process either hangs until the sleep ends or gets SIGKILLed by the orchestrator. With the select, cancellation is immediate and the shutdown is clean.
A helper is worth having, since this pattern appears everywhere:
func sleepCtx(ctx context.Context, d time.Duration) error {
t := time.NewTimer(d)
defer t.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
return nil
}
}
time.NewTimer with a deferred Stop rather than time.After matters in a hot loop: time.After allocates a timer that is not garbage collected until it fires, so a loop that cancels early accumulates them. For a loop running once every thirty seconds it is irrelevant; for one running thousands of times a second it is a leak.
Ticker for periodic work
A loop that does work then sleeps drifts, because the interval becomes work time plus sleep time. Ticker fires on a fixed schedule instead.
func poll(ctx context.Context) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := doWork(ctx); err != nil {
log.Printf("poll: %v", err)
}
}
}
}
Always defer ticker.Stop(). An unstopped ticker keeps its goroutine and timer alive for the life of the program — a genuine leak, and one that only shows up under memory profiling.
One caveat: if the work takes longer than the interval, ticks are dropped rather than queued. That is usually the behaviour you want for polling, but it means “every 10 seconds” becomes “every 10 seconds or when the previous run finishes, whichever is later”. If you need to know that you are falling behind, measure the work duration and log when it exceeds the interval.
Retry with backoff
func retry(ctx context.Context, attempts int, fn func() error) error {
var err error
for i := 0; i < attempts; i++ {
if err = fn(); err == nil {
return nil
}
if i == attempts-1 {
break
}
// Exponential backoff, capped, with jitter
delay := time.Duration(1<<uint(i)) * time.Second
if delay > 30*time.Second {
delay = 30 * time.Second
}
delay += time.Duration(rand.Int63n(int64(delay / 2)))
if cerr := sleepCtx(ctx, delay); cerr != nil {
return cerr
}
}
return err
}
Three things that are not optional. The cap — without it, 1<<uint(i) overflows into a nonsensical duration within about sixty attempts. The jitter — without it, every client that failed simultaneously retries simultaneously and rebuilds the load spike that caused the failure. The context — so a shutdown does not wait out a thirty-second backoff.
Also worth checking whether the error is retryable at all. Retrying a 400 five times with backoff is thirty seconds spent confirming that your request is still malformed.
Where Sleep in tests goes wrong
time.Sleep in a test is a guess. Too short and the test is flaky; too long and the suite crawls. Both happen, often in the same test on different machines.
// Flaky: hopes 100ms is enough
go doAsyncThing()
time.Sleep(100 * time.Millisecond)
assert(result)
// Deterministic: wait for the actual signal
done := make(chan struct{})
go func() { doAsyncThing(); close(done) }()
select {
case <-done:
assert(result)
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for async work")
}
The channel proves the work finished; the time.After is a failure deadline rather than an expected duration, so it can be generous without slowing the passing case. A test that passes takes microseconds; only a broken one takes two seconds.
For time-dependent logic, inject a clock interface rather than sleeping. Then tests advance time instantly and a test for a 24-hour expiry does not take 24 hours.
When Sleep is genuinely fine
- Rate limiting a simple script with no cancellation to worry about.
- A short pause in
mainbefore exit while a buffer flushes. - Demonstrating concurrency in an example.
- Any short-lived program where a SIGTERM arriving mid-sleep does not matter.
The dividing line is whether anything needs to interrupt it. A CLI tool that runs for two seconds can sleep freely. A long-running service cannot, because a deploy is a cancellation and an uncancellable sleep turns a graceful shutdown into a forced kill.
That is the operational cost of getting it wrong: a service that ignores SIGTERM gets SIGKILLed after the grace period, so in-flight requests are severed and every deploy drops a few. It looks like intermittent errors correlated with deploys, which is a confusing thing to debug — and much easier to spot when the deploy history and the runtime logs sit in the same place, as they do per deploy on RunxBuild.
How this fits the rest of the stack
time.Sleep takes a Duration, so always multiply by a unit — time.Sleep(2) is two nanoseconds. It cannot be cancelled, so in any long-running code use select on ctx.Done() and a timer instead. Use Ticker with a deferred Stop for periodic work, cap and jitter your backoffs, and replace sleeps in tests with a channel plus a generous failure deadline.
A service that cannot be interrupted mid-sleep is a service that cannot shut down cleanly, and that shows up as errors on every deploy. If you are sizing up what that service and its database cost, the RunxBuild hosting calculator lists them as separate line items.
Useful related references:
- JavaScript Sleep: Why There Is No sleep() and What To Use Instead
- Golang Hosting in 2026: Where a Go App Actually Wants to Live
- Golang Environment Variables: The Boring Truth About os.Getenv, the Library Most Teams Reach For, and One They Should Not
- Services on RunxBuild
FAQ
How do I sleep for a number of seconds in Go?
time.Sleep(2 * time.Second). Always multiply by a unit constant, because time.Duration counts nanoseconds — time.Sleep(2) compiles fine and sleeps for two nanoseconds. From an int variable, write time.Duration(n) * time.Second.
Can time.Sleep be interrupted in Go?
No. Once a goroutine is in time.Sleep nothing can wake it early — not context cancellation, not a signal. Use select with ctx.Done() and a time.Timer channel instead, so cancellation returns immediately rather than after the full duration.
What is the difference between time.Sleep and time.After?
time.Sleep blocks the goroutine and returns nothing. time.After returns a channel that receives after the duration, so it can be used in a select alongside a cancellation channel. In hot loops prefer time.NewTimer with a deferred Stop, since time.After leaks a timer until it fires.
Should I use time.Sleep or time.Ticker for periodic work?
Ticker for anything periodic. A loop that works then sleeps drifts, because the real interval becomes work time plus sleep time. A Ticker fires on a fixed schedule. Always defer ticker.Stop(), or the timer and its goroutine leak for the life of the program.
How do I avoid time.Sleep in Go tests?
Signal completion with a channel and select on it against a generous time.After deadline. The test passes as soon as the work is done rather than after a fixed guess, and the timeout only applies when something is actually broken. For time-dependent logic, inject a clock so tests can advance time instantly.