Most Go devs call context.Background() on autopilot and never think twice. But context isn’t just plumbing — it’s the backbone of cancellation, deadlines, and request-scoped values across goroutines and service boundaries. In distributed systems, where one user request fans out across five-plus services, context prevents cascading failures, goroutine leaks, and silent resource exhaustion.
The context API, the patterns that matter for distributed systems, and the pitfalls that trip up even experienced Gophers — code first, opinions second.
Problem: the leaky fan-out
An HTTP handler queries three downstream services in parallel:
func handler(w http.ResponseWriter, r *http.Request) {
ch := make(chan result, 3)
go fetch("service-a", ch)
go fetch("service-b", ch)
go fetch("service-c", ch)
// collect results…
}
Then the client disconnects after 200ms. The goroutines still talking to service-b and service-c keep right on running — consuming memory, open connections, and CPU. That’s a goroutine leak, and in production it compounds until the process OOMs.
Context fixes this by giving every goroutine a shared signal to stop. context.Context provides a Done() channel that propagates from parent to child — when the client disconnects, the handler cancels the context and every goroutine listening on ctx.Done() exits immediately.
Context in practice: The core API
ctx := context.Background()
// Root context: no cancel, no deadline, no values.
// Use in main(), init(), or tests.
ctx = context.TODO()
// Placeholder when you're unsure which context to use.
ctx, cancel := context.WithCancel(ctx)
// Manual cancellation.
// Useful for goroutine lifecycle management or early bailout.
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 2*time.Second)
// Automatically cancels after the timeout.
// Most common for HTTP and gRPC requests.
defer cancel()
ctx, cancel = context.WithDeadline(ctx, time.Now().Add(2*time.Second))
// Cancels at a specific point in time.
// Similar to WithTimeout, but uses an absolute deadline.
defer cancel()
ctx = context.WithValue(ctx, traceKey, traceID)
// Attach request-scoped metadata (e.g. trace IDs, auth info,
// feature flags). Avoid using for optional parameters.
Every With* function returns a derived context. When a parent is canceled, all children derived from it are canceled too.
Cancellation tree
The fan-out pattern done right:
func fanOut(ctx context.Context, services []string) []result {
ch := make(chan result, len(services))
for _, svc := range services {
go func(s string) {
select {
case ch <- fetch(ctx, s):
case <-ctx.Done():
// parent canceled — bail
}
}(svc)
}
// collect or timeout
return collect(ctx, ch, len(services))
}
Every select with ctx.Done() follows the same pattern: listen and return early.
HTTP handler with context propagation
func handleSearch(w http.ResponseWriter, r *http.Request) {
// Request context is already canceled if the client disconnects.
ctx := r.Context()
// Add a timeout for downstream work.
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
// Attach request-scoped metadata.
traceID := extractTraceID(r)
ctx = context.WithValue(ctx, traceKey, traceID)
// Fan out requests to downstream services.
results := fanOut(ctx, services)
// If the client disconnects or the timeout expires,
// ctx is canceled and all goroutines should exit early.
_ = results
}
The key insight is that r.Context() is already wired to the client connection — you derive from it rather than starting from scratch.
Retry loop with context
Context-aware retries prevent runaway retry storms:
func retryWithBackoff(
ctx context.Context,
maxRetries int,
fn func() error,
) error {
for i := 0; i < maxRetries; i++ {
if err := fn(); err == nil {
return nil
}
select {
case <-time.After(backoff(i)):
// Retry after the backoff delay.
case <-ctx.Done():
// Stop retrying if the context is canceled
// or the deadline is exceeded.
return ctx.Err()
}
}
return fmt.Errorf("max retries exceeded")
}
The select ensures timeout or cancellation cuts through the backoff immediately — without it, the caller waits for every attempt.
Deadline propagation across services
gRPC propagates context deadlines automatically. When an incoming RPC carries a deadline, outgoing RPCs derive from the same context — the deadline is converted to a timeout with elapsed time already deducted. No manual plumbing required.
WithValue: the right way
context.WithValue is for request-scoped metadata that crosses process boundaries, not for optional parameters:
Trace IDs
Auth principals
Request-scoped feature flags
Collision-resistant key pattern:
// package trace
type traceKeyType struct{}
var traceKey = traceKeyType{}
func WithTraceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, traceKey, id)
}
func TraceIDFromContext(ctx context.Context) (string, bool) {
id, ok := ctx.Value(traceKey).(string)
return id, ok
}
An unexported key type prevents collisions — never use strings or built-in types as keys.
Pros & Cons
Conclusion
Context is for cancellation first, metadata second, and nothing third. Every goroutine that performs I/O needs to listen on ctx.Done() — that’s the rule with no exceptions. Use WithValue sparingly and only for cross-boundary metadata like trace IDs or auth principals, and never store contexts in structs (the sole exception is backwards compatibility, as in http.Request).
Rule of thumb: if you’re putting something in context because passing it as a parameter is inconvenient, you’re doing it wrong.
References
Sameer Ajmani, “Go Concurrency Patterns: Context”, Go Blog, 2014. https://go.dev/blog/context
Sameer Ajmani, “Go Concurrency Patterns: Pipelines and cancellation”, Go Blog, 2014. https://go.dev/blog/pipelines
Jean Barkhuysen, Matt T. Proud, “Contexts and structs”, Go Blog, 2021. https://go.dev/blog/context-and-structs
Go Team, “context package”, pkg.go.dev. https://pkg.go.dev/context
gRPC Authors, “Deadlines”, gRPC.io, 2025. https://grpc.io/docs/guides/deadlines/
Jon Calhoun, “Pitfalls of context values and how to avoid or mitigate them in Go”, Calhoun.io, 2023. https://www.calhoun.io/pitfalls-of-context-values-and-how-to-avoid-or-mitigate-them/