context.Context: Cancellation as a First-Class API
context.Context is how Go services stop work cooperatively when a client leaves, a deadline expires, or a process shuts down.
It is not optional sugar in server code - it is the standard contract for timeouts, cancellation, and request-scoped metadata across HTTP handlers, database calls, gRPC, and background workers.
context Basics collects runnable snippets; sibling articles cover HTTP propagation, cross-service deadlines, values, database/gRPC integration, testing, and team conventions.
Summary
context.Contextis an immutable handle passed down call stacks. It signals when work should stop and carries a small set of request-scoped values.- Insight: Without context, goroutines keep running after clients disconnect, connection pools fill with abandoned queries, and shutdown becomes force-kill instead of graceful drain.
- Key Concepts: cancellation, deadline, Done channel, WithCancel, WithTimeout, WithValue, propagation.
- When to Use: Every function that can block on I/O, RPC, database access, or sleeps in a request or job path should accept
ctx context.Contextas its first parameter. - Limitations/Trade-offs: Context values are untyped and easy to abuse; storing context in structs breaks lifetime rules; child cancellation does not cancel the parent.
- Related Topics: HTTP middleware, gRPC interceptors,
database/sql, errgroup, graceful shutdown, structured logging with request IDs.
Foundations
Picture a request entering your service as a tree of function calls.
At the root, something creates a context - http.Request.Context() for HTTP, context.Background() for process startup, or context.WithTimeout for a bounded job.
Each downstream call receives that context (or a child derived from it).
When the root cancels - client disconnect, timeout, or explicit cancel() - every descendant that respects ctx.Done() should stop promptly.
A context carries three concerns:
- Cancellation - a
Done()channel closes when work should end. - Deadline - an optional wall-clock end time via
Deadline(). - Values - a small key-value bag for request metadata (use sparingly).
Contexts form a parent-child chain.
context.WithCancel(parent) returns a child and a cancel function.
Calling cancel() on the child does not cancel the parent, but cancelling the parent cancels all descendants.
That one-way propagation is deliberate: upstream owns the lifecycle.
Mechanics & Interactions
The idiomatic function signature is:
func FetchUser(ctx context.Context, id string) (*User, error)Callers pass ctx unchanged or wrap it:
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()Blocking code should select on ctx.Done() or use APIs that accept context (db.QueryContext, gRPC stubs, http.NewRequestWithContext).
When ctx ends, return ctx.Err() - usually context.Canceled or context.DeadlineExceeded.
HTTP servers wire this automatically.
net/http gives each request r.Context(), which cancels when the client closes the connection or the server hits ReadHeaderTimeout/WriteTimeout boundaries.
Frameworks like chi, gin, and echo expose the same request context through their handler APIs.
google.golang.org/grpc threads metadata and deadlines through context.Context on every RPC.
database/sql methods ending in Context honor cancellation at the driver level when supported.
Shutdown paths derive a timeout context from context.Background():
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)| Mechanism | What it signals | Typical source |
|---|---|---|
WithCancel | Manual stop | Handler exit, errgroup failure |
WithTimeout | Relative budget | Per-RPC client call |
WithDeadline | Absolute end time | Upstream grpc-timeout header |
Request.Context() | Client disconnect | net/http |
context.WithoutCancel | Detach for cleanup | Post-response logging (Go 1.21+) |
Advanced Considerations & Applications
Production services stack deadlines at each hop.
An edge gateway might allow 5s; an internal service gets 3s after middleware overhead; a database query gets 500ms.
Each layer should set a deadline shorter than its parent so slack exists for serialization and retries.
controller-runtime reconcilers use context to stop work when a manager shuts down or a lease is lost.
kubebuilder-generated controllers pass ctx into client calls so API server watches respect cancellation.
golangci-lint includes checks (via staticcheck and custom linters) for context misuse - passing context.Background() inside handlers, storing context in structs, or ignoring ctx in loops.
For values, the Go team recommends unexported key types to avoid collisions:
type ctxKey int
const requestIDKey ctxKey = 1Never use context values for optional parameters that belong in function arguments.
Pair context with observability: extract request IDs from context in middleware and attach them to structured logs and traces.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Context cancellation | Standard, composable | Requires discipline in every call | HTTP/gRPC/DB stacks |
time.After per call | Simple local timeout | No propagation to children | Single goroutine scripts |
Manual done channel | Custom lifecycle | Reinvents context | Legacy code paths |
| Process signals only | OS-level stop | No per-request granularity | CLI tools |
errgroup + context | Cancels siblings on error | Needs explicit group wiring | Parallel fan-out |
Common Misconceptions
- "Context is only for HTTP" - Any long-lived or blocking work benefits: workers, reconcilers, CLI commands with timeouts, and tests.
- "I can store context in a struct" - Context lifetime is request-scoped; storing it in a struct ties the struct to a stale cancellation scope.
- "context.Background() in handlers is fine" - It ignores client disconnect and server deadlines; always start from
r.Context(). - "Child cancel affects parent" - Only parent-to-child propagation exists; calling a child's
cancel()stops descendants, not ancestors. - "Context values replace config" - Values are for cross-cutting request metadata (trace IDs), not feature flags or business options.
- "Checking ctx.Done() once is enough" - Long loops and stream readers must poll or use context-aware APIs throughout.
FAQs
Why is context the first parameter?
Community convention and gofmt/review tooling expect ctx context.Context first.
It keeps the signal visible at every call site.
What is the difference between cancel and timeout?
WithCancel stops when you call cancel() or when the parent ends.
WithTimeout also sets a deadline and calls cancel automatically when time expires.
Should I always defer cancel()?
Yes for WithCancel, WithTimeout, and WithDeadline.
Deferred cancel() releases timer resources even if the deadline passed early.
What error should I return when ctx ends?
Return ctx.Err() unchanged when cancellation caused the failure.
Wrap with %w only if callers need extra context while preserving errors.Is.
Does database/sql respect context?
Methods like QueryContext pass cancellation to the driver when supported.
Always prefer *Context variants over blocking calls without context.
How does gRPC use context?
Deadlines and metadata ride on context.Context.
Client stubs derive timeouts from context; servers read them for enforcement.
Can I pass nil context?
Never - use context.Background() or context.TODO() at roots only.
Libraries should document that nil panics or is rejected.
What is context.TODO() for?
A placeholder when the correct parent context is unclear during refactors.
Replace with a real parent before shipping.
How do values collide across packages?
Use unexported custom key types in each package.
String keys are a collision risk in large codebases.
Does context replace errgroup?
No - errgroup uses context to cancel sibling goroutines on first error.
They complement each other.
Related
- context Basics - runnable WithCancel/WithTimeout examples
- Cancellation Propagation in HTTP Handlers - request-scoped ctx
- Deadlines & Timeouts Across Service Boundaries - multi-hop budgets
- context Values: When and When Not - metadata without abuse
- context in database/sql & gRPC - driver and RPC integration
- context Package Best Practices - team conventions
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).