Skip to content

Intermediate · Concurrency: goroutines, channels and context · topic 30 of 46 · 35 min

context: timeouts and cancellation

Explanation

A context.Context carries a deadline and a cancel signal across function calls and goroutines. Create one with context.WithTimeout or WithCancel, always defer cancel(), pass ctx as the first parameter, and stop work when ctx.Done() is closed. HTTP servers, HTTP clients and database/sql all accept a context.

The problem

A user opens a page; your handler calls a database and two other services. The user closes the tab after one second. Without a way to say "stop", all that work keeps running for nothing, using connections and CPU. Or a bank API hangs, and your request waits forever. You need a way to say "give up after 2 seconds" or "the caller is gone, stop now", across every function and goroutine involved. That is context.

Think of it like this: A context is like a token number at a government office with an expiry time printed on it. Every counter the token goes to can see the expiry. If you leave the queue, the token is cancelled and every counter stops working on your file.

Creating contexts

FunctionUse
context.Background()The empty root context, in main, tests and start-up code
context.TODO()A placeholder when you are not sure yet which context to use
context.WithCancel(parent)Returns ctx and cancel(); calling cancel stops ctx and its children
context.WithTimeout(parent, d)Cancelled automatically after duration d
context.WithDeadline(parent, t)Cancelled automatically at time t
context.WithValue(parent, key, val)Carries request-scoped data such as a request ID (use sparingly)
go
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // ALWAYS: releases resources even if work finishes early

err := chargeCard(ctx, 49900)
if errors.Is(err, context.DeadlineExceeded) {
	fmt.Println("bank too slow, try again")
}
go vet warns if you forget to call cancel (the lostcancel check).

Listening for cancellation

ctx.Done() returns a channel that is closed when the context is cancelled or its deadline passes. ctx.Err() then says why: context.Canceled or context.DeadlineExceeded. Long-running code checks Done in a select.

go
func chargeCard(ctx context.Context, paise int64) error {
	result := make(chan error, 1)
	go func() {
		time.Sleep(3 * time.Second) // pretend slow bank call
		result <- nil
	}()
	select {
	case err := <-result:
		return err
	case <-ctx.Done():
		return ctx.Err() // context.DeadlineExceeded or context.Canceled
	}
}
With a 2-second timeout this returns context deadline exceeded after 2 seconds instead of waiting 3.
go
func processAll(ctx context.Context, ids []int) error {
	for _, id := range ids {
		if err := ctx.Err(); err != nil {
			return err // stop between items once cancelled
		}
		process(id)
	}
	return nil
}
In a loop, checking ctx.Err() between items is enough.

Contexts form a tree

Every derived context has a parent. Cancelling a parent cancels all its children, but cancelling a child does not affect the parent. A child's deadline can be shorter than its parent's, never longer: the earliest deadline wins.

Where contexts come from in real code

go
func handler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context() // cancelled if the client disconnects

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

	row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", 42)
	// ...
	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/rates", nil)
	// resp, err := client.Do(req)
	_ = row
	_ = req
}
The same ctx flows into the database query and the outgoing HTTP call, so one timeout covers the whole request.

The rules

  1. Pass ctx as the first parameter, named ctx: func Charge(ctx context.Context, amount int64) error.
  2. Do not store a context in a struct field; pass it to each call that needs it.
  3. Always call cancel, usually with defer, right after creating a cancellable context.
  4. Never pass a nil context. Use context.Background() or context.TODO().
  5. Use WithValue only for request-scoped data that crosses API boundaries (request ID, auth info), never for optional function parameters. Use your own unexported key type to avoid clashes.
Tip: Newer helpers: context.WithCancelCause (Go 1.20) lets you record why something was cancelled; context.AfterFunc and context.WithoutCancel arrived in Go 1.21.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

context: timeouts and cancellation · 1/7

context

Some work must stop: when it takes too long, or when the user leaves. context carries that stop signal.

Prefer a video by a teacher? Find free videos on “golang context withtimeout withcancel tutorial” on YouTube ↗

In real life: A train-search API that answers within 2 seconds

A travel app searches three partner APIs for train availability. The whole search must answer within 2 seconds, even if one partner hangs. A context with a 2-second timeout is passed to every partner call; slow partners are cut off, and the app shows whatever results arrived in time.

go
package main

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

func search(ctx context.Context, partner string, delay time.Duration) (string, error) {
	select {
	case <-time.After(delay): // pretend network call
		return partner + ": 12 seats", nil
	case <-ctx.Done():
		return "", fmt.Errorf("%s: %w", partner, ctx.Err())
	}
}

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

	delays := map[string]time.Duration{"A": 500 * time.Millisecond, "B": 3 * time.Second}
	for _, p := range []string{"A", "B"} {
		res, err := search(ctx, p, delays[p])
		if err != nil {
			fmt.Println("skipped:", err)
			continue
		}
		fmt.Println(res)
	}
}
Prints A: 12 seats, then skipped: B: context deadline exceeded. (In real code the searches would run in parallel goroutines.)

Practice (0/4 done)

Task 1 · Question

Where should ctx go in a function signature?

Task 2 · Question

What does ctx.Err() return after a WithTimeout context times out?

Task 3 · Question

Which line is missing? ctx, cancel := context.WithTimeout(context.Background(), time.Second) result, err := fetch(ctx)

Task 4 · Practice

Write func countSlowly(ctx context.Context, n int) (int, error) that counts from 1 to n, sleeping 100 ms per step, and stops early with ctx.Err() if the context is cancelled. Call it with a 350 ms timeout and print what it returns.

Quick quiz

Question 1

1.What happens to child contexts when the parent is cancelled?

Question 2

2.Which context does an HTTP handler normally start from?

Question 3

3.What is context.WithValue meant for?

Question 4

4.Can a child context have a later deadline than its parent?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What happens to child contexts when the parent is cancelled?

Answer: They are cancelled too

Cancellation flows down the tree.

Which context does an HTTP handler normally start from?

Answer: r.Context()

r.Context() is cancelled when the client goes away or the server shuts down.

What is context.WithValue meant for?

Answer: Request-scoped data like a request ID

Keep values small and request-scoped; pass normal parameters explicitly.

Can a child context have a later deadline than its parent?

Answer: No, the earlier deadline always wins

A child is cancelled no later than its parent.

Is an http.Client safe to use from many goroutines?Related

Answer: Yes

Clients and Transports are safe for concurrent use.

From Calling other APIs safely with the HTTP client

What is a goroutine leak?Related

Answer: A goroutine blocked forever that never ends, holding memory

Leaked goroutines pile up and waste memory over time.

From select: waiting on several channels

What does launch return?Related

Answer: A Job

A Job lets you join or cancel the coroutine.

From launch, async and structured concurrency

How do you cancel the notes request when the user leaves the page in React?Related

Answer: Create an AbortController, pass its signal to fetch, and call abort() in the useEffect cleanup

The cleanup runs when the component leaves the screen; abort() cancels the fetch, which rejects with an AbortError you can ignore.

From Loading, errors and slow networks

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Concurrency: goroutines, channels and context”. The module exam comes next.
  2. Module 6 exam: Concurrency · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Standard library essentials. 46 topics left in the course before the final exam.

← Previous: Mutex, Once, atomic and the race detector