How to handle timeouts with context in Go

When writing Go applications that rely on I/O, remote calls or long-running operations, you can’t always wait forever. Sometimes you need to set a deadline, a point in time after which you should stop trying and move on.

That’s exactly what context.WithTimeout is built for.

What is context.WithTimeout?

context.WithTimeout creates a child context from an existing one (often context.Background() or a request-scoped context) that automatically cancels itself after a certain duration.This is useful when you want to limit how long an operation can take, like an HTTP call, a DB query or a background task.

Keep in mind: Cancelling the child context does not affect the parent but if the parent is cancelled first, the child will be cancelled too.

Here’s how it works:

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

defer cancel()

The context ctx will be cancelled automatically after 3 seconds, unless cancelled manually earlier by cancel().

Any goroutine or function that listens to ctx.Done() will receive the cancellation event and should exit.

Example: Timeout for a long-running operation

Let’s simulate a long task that might take too long to finish.

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    done := make(chan struct{})

    go func() {
        fmt.Println("Starting a long task...")

        time.Sleep(5 * time.Second) // Simulate something slow

        fmt.Println("Finished task")

        close(done)
    }()

    select {
    case <-done:
        fmt.Println("Task completed in time.")

    case <-ctx.Done():
        fmt.Println("Timeout exceeded:", ctx.Err())
    }
}

Now, if we execute this code we get the following output:

Starting a long task...
Timeout exceeded: context deadline exceeded

Why use timeouts?

Using timeouts helps protect your system from:

  • Hanging external services: Waiting for a database query result or from an external API you’re using.
  • Slow I/O operations.
  • Infinite loops or bugs.
  • Resource leaks.

Timeouts give your application predictability: you know how long you’ll wait and what happens when time runs out.

What if the operation finishes early?

If your operation completes before the timeout, calling cancel() is still important to avoid leaking the context’s internal timer.

That’s why you should always call defer cancel() even when using timeouts: it cleans up resources no matter what. And this is a really common mistake people make when dealing with contexts. They usually get the child context, use it in whatever situation they need and completely ignore the cancel function.

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
	// This right here is a recipe for memory leaks in production:
    ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)

    done := make(chan struct{})

    go func() {
        fmt.Println("Starting a long task...")

        time.Sleep(5 * time.Second) // Simulate something slow

        fmt.Println("Finished task")

        close(done)
    }()

    select {
    case <-done:
        fmt.Println("Task completed in time.")

    case <-ctx.Done():
        fmt.Println("Timeout exceeded:", ctx.Err())
    }
}

If you execute the previous example, you’ll get the exact same output and behavior. The main difference is that, when you execute this code over and over again in the same webserver, each API call leaks one context cancellation. Over time, you’ll see the memory going up and up until you run out of memory and you need to restart the server.

Summary

  • Use context.WithTimeout to automatically cancel operations after a given time.
  • Always call cancel() to clean up the context’s timer.
  • Combine it with select to gracefully handle timeouts in your functions.
  • This is essential for building resilient, safe, and production-grade Go applications.

Whether you’re querying a database, calling a third-party API or processing user data, don’t let your app wait forever. Timeouts protect your system. Use them wisely.

See you next time!

Leave a Comment

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

Scroll to Top