When you need a quick and simple way to handle timeouts in Go but without building full-blown context trees then time.After
can be a clean and effective tool.
While context.WithTimeout is the idiomatic way to manage timeouts across API boundaries, sometimes you just want a lightweight mechanism for delaying an operation or waiting for something to complete, but only for a limited time.
That’s where time.After
comes into play.
What is time.After?
The time.After(d Duration)
function returns a channel that delivers the current time after the specified duration has passed.
It’s perfect for:
- Timing out a select statement.
- Adding a fallback delay.
- Sleeping with better control.
- Quick, one-off timeout logic.
Example: Simple timeout for a task
Here’s a basic example where we simulate a task and wait for it to finish, but give up after 2 seconds.
package main
import (
"fmt"
"time"
)
func main() {
done := make(chan string)
// Simulate a long-running task
go func() {
fmt.Println("Long task started.")
time.Sleep(3 * time.Second)
done <- "Task completed"
}()
// We are going to wait exactly 2 seconds for the task to complete:
timeoutDuration := 2 * time.Second
select {
case result := <-done:
fmt.Println(result)
case <-time.After(timeoutDuration):
fmt.Println("Timeout! Task took too long.")
}
fmt.Println("All done.")
}
Let’s execute this code and check the output:
As you can see, the task took 3 seconds but our patience ran out at 2.
If now we work in our patience a little bit and increase our timeout from 2 seconds to 4 seconds, let’s see what happens. Make the following change to the code:
// We are going to wait exactly 4 seconds for the task to complete:
timeoutDuration := 4 * time.Second
And the new output when we execute our code:
Long task started.
Task completed
All done.
How does it work?
time.After(d Duration)
returns a channel oftime.Time
that we can use in ourselect
statement.- After 2 seconds, that channel sends a single
time.Time
value. - You can select on that channel to react to the timeout.
It’s a blocking mechanism, ideal for select-based logic in goroutines or simple synchronous flows.
Be cautious with time.After
Remember this: time.After
creates a timer. Don’t ignore it.
Every call to time.After
creates a new internal timer. If the result is never read from the channel, the timer will stick around and can cause a memory leak in long-running apps.
If you expect to cancel the operation before timeout, consider using time.NewTimer()
instead, which lets you manually Stop()
the timer:
timer := time.NewTimer(5 * time.Second)
// ...
timer.Stop()
Otherwise, in short-lived or simple code, time.After
is totally fine.
Common use cases
- Retry logic with backoff.
- Timeout for blocking calls like read or select.
- Waiting with a fallback duration.
- Adding “grace periods” before exiting a loop.
- Timeouts that are not tied to a given Context.
Summary
time.After
is a simple way to implement timeouts without using context.- It returns a channel that fires once after a duration.
- Combine it with the
select
statement to build clean timeout behavior. - Be careful not to leak timers in long-lived code.
It’s not a replacement for context.WithTimeout but for quick operations, test scripts or isolated routines, it’s a solid tool to have in your toolbox.
See you next time!