Using time.After for Simple Timeouts in Go

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 of time.Time that we can use in our select 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!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top