The pause button for goroutines
You are building a bot that scrapes a website. You hit the API too fast, and the server returns a 429 status code. You need to slow down. Or you are retrying a flaky database connection and want to wait a moment before trying again. You reach for time.Sleep. It is the simplest way to pause execution in Go.
time.Sleep suspends the current goroutine for a specified duration. It does not stop the whole program. Other goroutines keep running while the sleeping one waits. When the time is up, the runtime wakes the goroutine back up and it continues from where it left off.
How sleep works under the hood
time.Sleep takes a time.Duration argument. A duration is just an int64 representing nanoseconds. The standard library provides constants like time.Second, time.Millisecond, and time.Hour so you don't have to count zeros. time.Sleep(2 * time.Second) sleeps for two seconds.
When you call time.Sleep, the Go runtime registers a timer and moves the current goroutine to a wait state. The scheduler sees that this goroutine is blocked and picks another ready goroutine to run on the processor core. This is why time.Sleep is cooperative. It yields the thread voluntarily. The program stays responsive because the scheduler can run other work while one goroutine naps.
When the timer expires, the runtime moves the goroutine back to the ready queue. The scheduler eventually picks it up and resumes execution. The sleep duration is a minimum. The goroutine sleeps for at least the requested time. If the system is under heavy load, the goroutine might wake up later. It will never wake up early.
Trust gofmt. Argue logic, not formatting. The tool handles indentation and style so you can focus on the timing logic.
Minimal example
Here is the simplest usage: spawn a goroutine, sleep, and print.
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Starting...")
// Pause this goroutine for 2 seconds.
// Other goroutines continue running during this wait.
time.Sleep(2 * time.Second)
fmt.Println("Resumed after 2 seconds")
}
The program prints "Starting...", waits two seconds, then prints the resume message. If you run this, you will see the delay in the terminal output. The time package is imported because time.Sleep and time.Second live there.
Realistic retry loop with context
In production code, you rarely sleep in isolation. You usually sleep inside a retry loop or a polling worker. These operations often need to support cancellation. If the user cancels the request or the server shuts down, the goroutine should stop sleeping and exit.
time.Sleep cannot be interrupted. If you sleep for ten minutes and the context cancels after one second, the goroutine still waits for the full ten minutes. You must check the context before sleeping.
Here is a retry function that respects cancellation.
// pollWithBackoff retries a request until success or context cancellation.
func pollWithBackoff(ctx context.Context, url string) error {
delay := time.Second
for {
// Check if the caller cancelled before sleeping.
// Sleeping on a cancelled context wastes time and blocks shutdown.
if ctx.Err() != nil {
return ctx.Err()
}
// Perform the work.
// If this succeeds, the function returns immediately.
if err := fetch(url); err == nil {
return nil
}
// Wait before the next attempt.
// The duration grows exponentially to avoid hammering the server.
time.Sleep(delay)
delay *= 2
}
}
The function checks ctx.Err() at the top of the loop. If the context is done, it returns the error immediately. This prevents the goroutine from sleeping when it should be exiting. The fetch call is a placeholder for your actual work. If it returns an error, the loop sleeps and retries with a longer delay.
Context is plumbing. Run it through every long-lived call site. The context.Context parameter always goes first, conventionally named ctx. Functions that take a context should respect cancellation and deadlines.
Testing with deterministic time
Tests that use time.Sleep are slow and flaky. They wait for real time to pass. If a test sleeps for five seconds, it takes five seconds to run. If the test suite has many sleeps, the feedback loop becomes painful. Worse, sleeps in tests can hide race conditions because the timing changes between runs.
The testing/synctest package provides a way to mock time. It replaces time.Sleep with a deterministic version that advances a fake clock. Tests run instantly and behave the same way every time.
Use synctest.Sleep in your tests instead of time.Sleep. Combine it with synctest.Wait to advance the clock manually. This gives you full control over time progression.
import "testing/synctest"
func TestPollWithBackoff(t *testing.T) {
// Create a context that cancels after a short duration.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Start the poller in a goroutine.
errCh := make(chan error, 1)
go func() {
errCh <- pollWithBackoff(ctx, "http://example.com")
}()
// Advance the fake clock by 1 second.
// The poller sleeps, then wakes up and retries.
synctest.Sleep(time.Second)
// Cancel the context to stop the poller.
cancel()
// Wait for the goroutine to finish.
err := <-errCh
if err != context.Canceled {
t.Errorf("expected context.Canceled, got %v", err)
}
}
The test uses synctest.Sleep to move time forward without waiting. The poller wakes up instantly when the clock advances. The test cancels the context and verifies that the poller returns the cancellation error. This pattern makes tests fast and reliable.
Common pitfalls and compiler errors
Passing a raw integer to time.Sleep triggers a compiler error. The function expects a time.Duration, not an int. If you write time.Sleep(1000), the compiler rejects the program with cannot use 1000 (untyped int constant) as time.Duration value in argument. You must multiply by a unit like time.Millisecond.
Forgetting the unit is a common mistake. time.Sleep(1000) would sleep for 1000 nanoseconds if it compiled, which is one microsecond. That is likely not what you intended. Always use the constants. time.Sleep(1000 * time.Millisecond) is clear and correct.
time.Sleep is not precise. It sleeps for at least the requested duration. The actual sleep time can be longer if the scheduler is busy or the system is under load. Do not use time.Sleep for high-precision timing. If you need millisecond accuracy, time.Sleep might not be enough.
Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path. If a goroutine sleeps and then waits on a channel, and the channel is never sent to or closed, the goroutine stays alive forever. This consumes memory and prevents the program from shutting down cleanly. Use context cancellation or a done channel to break the wait.
The worst goroutine bug is the one that never logs. Add logging to long-running goroutines so you can detect leaks and hangs in production.
When to use sleep versus alternatives
Go provides several ways to wait. Pick the right tool based on your cancellation and synchronization needs.
Use time.Sleep when you need a simple pause in a single goroutine and don't need to cancel the wait.
Use time.After or time.NewTimer when you need to wait but also want to respond to other events or cancellation via a channel.
Use time.Ticker when you need to perform an action repeatedly at fixed intervals.
Use context.WithTimeout when the wait is part of a larger operation that must respect a deadline.
Use a blocking channel receive when one goroutine needs to wait for a signal from another goroutine.
Use testing/synctest.Sleep in tests to make time deterministic and fast.
Sleep yields the thread. Don't hold locks while sleeping. If you hold a mutex and call time.Sleep, other goroutines waiting on that lock are blocked for the entire duration. This creates unnecessary contention and can lead to deadlocks if the lock is part of a cycle. Release locks before sleeping.