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
}
| Method | Returns |
|---|---|
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()formain,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 likeBackground; 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 vetdoes not catch this;staticcheckdoes.) - Wrap access in typed helper functions like
WithRequestIDandRequestID, so callers never seeanyor 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.Contextis 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
nilas a context. Usecontext.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 whenctxis. Use it for work that must finish after the request ends, like writing an audit log.context.AfterFunc(ctx, f)runsfin its own goroutine oncectxis done, and returns astopfunction to unregister it.
Common mistakes
- Not calling
cancel. Alwaysdefer cancel()right afterWithCancel,WithTimeoutorWithDeadline. - Starting a goroutine that ignores
ctx. If it blocks without selecting onctx.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 thectxyou were given. - Comparing errors with
==. Useerrors.Is(err, context.DeadlineExceeded); most libraries wrap it. - Using
WithValuefor 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.