Menu

Golang Context: Cancellation, Timeouts and Values

How context.Context carries cancellation, deadlines and request-scoped values through a Go program: Background, WithCancel, WithTimeout, WithValue, ctx.Done in select, and context in HTTP servers and clients.

This page includes runnable editors - edit, run, and see output instantly.

A timeout in ten lines

The job of context.Context is to tell code when to stop. Here a slow operation gets 50 ms, and gives up when the context says so:

The first call finishes in 10 ms and returns rows <nil>. The second would need 200 ms, but the context expires at 50 ms (counted from when it was created), so it returns context deadline exceeded.

Nothing is stopped by force. Go has no way to kill a goroutine from outside. A context is a signal, and code has to check it: by selecting on ctx.Done(), by checking ctx.Err() between steps, or by passing ctx to library calls (http.NewRequestWithContext, db.QueryContext, exec.CommandContext) that check it for you.

The Context interface

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}
MethodReturns
Done()a channel that is closed when the context is cancelled or times out (nil for a context that can never be cancelled)
Err()nil while active, then context.Canceled or context.DeadlineExceeded
Deadline()the deadline and true, or ok == false if there is none
Value(key)the value stored under key in this context or an ancestor, or nil

Contexts are immutable. You never change one; you derive a child from it with one of the With functions, and the child adds a cancel signal, a deadline or a value.

Where a context comes from

Every context tree starts at a root:

  • context.Background() for main, init, tests and servers' top-level setup.
  • context.TODO() when a function should take a context but the caller does not have one yet. It behaves exactly like Background; the name is a marker for later refactoring.

Inside an HTTP handler you do not create a root. You use r.Context(), which the server cancels when the client disconnects or the handler returns.

WithCancel: stop on demand

context.WithCancel returns a child context and a cancel function. Calling cancel closes the child's Done channel and the Done channels of everything derived from it.

The producer's send sits in a select next to ctx.Done(). That is what lets it stop: a bare out <- i would block forever once the consumer stopped reading, and the goroutine would leak. The final for range nums waits until the producer has closed the channel. While it drains, the producer may still manage to send a value or two, because when both cases of its select are ready Go picks one at random; cancellation is prompt, not instantaneous.

cancel is safe to call more than once and from any goroutine. Only the first call does anything.

WithTimeout and WithDeadline

WithTimeout(parent, d) is WithDeadline(parent, time.Now().Add(d)). Use a timeout for "at most this long" and a deadline when you have an absolute time.

Once the time passes, Done closes and Err returns context.DeadlineExceeded. If cancel is called first, Err returns context.Canceled. Check which one with errors.Is, because libraries usually wrap the error:

Always call cancel, even for a timeout that will fire on its own. The context holds a timer and a slot in its parent until one of them happens, and defer cancel() releases both as soon as the function returns. go vet reports a discarded cancel function: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak.

Children cannot outlive their parents

Contexts form a tree. Cancelling a parent cancels every descendant. A child can have a shorter deadline than its parent, never a longer one: the earlier deadline always wins.

This is what makes contexts useful across layers. An HTTP handler gets a context that dies with the request; a database call three layers down derives a 2-second timeout from it. If the client hangs up after 100 ms, the query is cancelled then, not two seconds later.

Always select on ctx.Done() when you block

Any goroutine that waits (on a channel send, a receive, a timer) should wait on ctx.Done() at the same time. For CPU-bound loops that never block, check ctx.Err() every so often:

for i, item := range items {
	if i%1000 == 0 {
		if err := ctx.Err(); err != nil {
			return err
		}
	}
	process(item)
}

Use time.After inside a select for a simple wait, but prefer a timer you can stop (or a context timeout) when the wait may be cancelled often.

Cancellation causes (Go 1.20 and 1.21)

ctx.Err() only says canceled or deadline exceeded. To record why, use the Cause variants:

WithCancelCause arrived in Go 1.20, WithTimeoutCause and WithDeadlineCause in Go 1.21. Err keeps returning the standard values so existing checks keep working; context.Cause gives the detail.

WithValue, sparingly

context.WithValue(parent, key, value) attaches one value. ctx.Value(key) looks it up through the chain of parents.

Rules for values:

  • Use an unexported type for keys, never a plain string. Two packages that both use "user" would overwrite each other. (go vet does not catch this; staticcheck does.)
  • Wrap access in typed helper functions like WithRequestID and RequestID, so callers never see any or the key.
  • Only store request-scoped data that passes through APIs: trace and request IDs, the authenticated user, a logger. Never optional parameters, database handles or configuration. Those belong in function arguments or struct fields, where the compiler can check them and readers can see them.
  • Lookup walks the chain one parent at a time, so every value you add makes lookups of the others a step longer.

Conventions

  • ctx context.Context is the first parameter of any function that does I/O, blocks, or calls something that does: func Fetch(ctx context.Context, url string) error.
  • Do not store a context in a struct. Pass it to each method call. A context belongs to one operation, and a struct usually outlives it. (The exception is a type that represents a single operation, like http.Request.)
  • Never pass nil as a context. Use context.TODO() if you have nothing better.
  • Return ctx.Err(), or wrap it with %w, when you stop because of the context, so callers can tell a timeout from a real failure.

Context in HTTP servers and clients

The server side: r.Context() is cancelled when the client disconnects, when the handler returns, or when an HTTP/2 stream is reset. The client side: http.NewRequestWithContext makes the request respect a timeout or cancellation. This program runs both ends through httptest:

The client gives up at 50 ms and closes the connection. The server notices, its request context is cancelled, and the handler stops instead of spending 450 more milliseconds on a report nobody will read. In a real handler you pass r.Context() down to every database and HTTP call, and they all stop together.

Other helpers (Go 1.21)

  • context.WithoutCancel(ctx) returns a context with the same values that is not cancelled when ctx is. Use it for work that must finish after the request ends, like writing an audit log.
  • context.AfterFunc(ctx, f) runs f in its own goroutine once ctx is done, and returns a stop function to unregister it.

Common mistakes

  • Not calling cancel. Always defer cancel() right after WithCancel, WithTimeout or WithDeadline.
  • Starting a goroutine that ignores ctx. If it blocks without selecting on ctx.Done(), cancelling does nothing and the goroutine leaks.
  • Creating a fresh context.Background() deep inside a call chain. It cuts the link to the caller's deadline and cancellation. Pass the ctx you were given.
  • Comparing errors with ==. Use errors.Is(err, context.DeadlineExceeded); most libraries wrap it.
  • Using WithValue for dependencies. A database handle hidden in a context is a parameter the compiler can no longer check.
  • Expecting cancellation to be instant. Code notices only at its next check. A long loop without a check keeps running.

Frequently Asked Questions

What is context used for in Go?

A context.Context tells a function and everything it calls when to give up: because the caller cancelled, because a deadline passed, or because the client disconnected. It can also carry request-scoped values such as a request ID. By convention it is the first parameter, named ctx.

What is the difference between context.Background and context.TODO?

Both return an empty context that is never cancelled and has no deadline or values. They behave identically. Background() is the root for main, tests and top-level setup. TODO() marks a place where a real context should be passed but the surrounding code does not have one yet, which makes it easy to find later.

Why do I have to call cancel after context.WithTimeout?

WithTimeout, WithDeadline and WithCancel register the new context with its parent and may start a timer. Calling cancel releases those resources as soon as you are done, instead of when the timeout fires or the parent is cancelled. Write defer cancel() right after creating it; go vet warns when a cancel function is dropped.

What does "context deadline exceeded" mean in Go?

It is the text of context.DeadlineExceeded, the error ctx.Err() returns once a context's deadline has passed. Functions that respect the context, such as HTTP clients and database drivers, return it (often wrapped) when they run out of time. Check for it with errors.Is(err, context.DeadlineExceeded).

Should I use context.WithValue to pass parameters?

No. Use it only for request-scoped data that crosses API boundaries and that functions in between do not need to know about, like a trace ID or an authenticated user. Anything a function needs to do its job belongs in its parameters, where the compiler checks it.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED