Go Context API: Timeouts, Cancellation & Values

Go Context API: Timeouts, Cancellation & Values
What is the Go Context Package?
The Go context package provides a standard way to carry deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines. A context.Context is passed as the first argument to any function that performs I/O, spawns goroutines, or calls external services. When the context is cancelled or times out, all operations watching it via ctx.Done() should stop and return immediately.
The Context API: Managing Lifecycles
In production-grade Go applications, particularly web servers and microservices, you rarely launch a Goroutine and let it run forever. What happens if a user cancels their HTTP request? Or what if a database query takes too long and you need to abort it?
The context package provides the standard way to carry deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
Universal Connectivity
The Basic Context Pattern
A Context is immutable. To add behavior (like a timeout or cancellation), you use "derivation" functions that return a new child Context and a CancelFunc.
Carrying Values Beyond Data
Wait, context can also carry values. This is used for "metadata" that is secondary to the primary function arguments, such as Trace IDs, User authentication tokens, or Logger instances.
[!WARNING] Do not use
context.WithValueto pass optional function parameters. Use it only for data that must transit across API boundaries and across the entire call stack.
Propagation through Layers
When you cancel a parent context, Go automatically cancels all its children. This allows you to stop an entire tree of Goroutines with a single signal.
Context Variants
Top-levelThe root context of any program. It is never cancelled and has no values.
Manual controlReturns a copy of the parent with a new Done channel that is closed when the cancel function is called.
Automatic controlCrucial for network calls. It automatically cancels itself after the specified duration elapses.
PlaceholderUse this when you're not sure which context to use yet or if the surrounding function hasn't been updated to take a context.
Best Practices
- Always call cancel(): If you use
WithCancel,WithDeadline, orWithTimeout, always call the cancel function (usually viadefer) to prevent resource leaks. - Context should be the first argument: By convention,
context.Contextshould be the first parameter of any function that performs I/O or takes time to complete. - Don't store context in structs: Pass it explicitly through the call stack.
Context in HTTP Handlers
The most common real-world use of context is in HTTP handlers. Go's net/http package automatically attaches a context to every incoming request. When the client disconnects, the context is automatically cancelled. Your handler should respect this:
This pattern ensures that if a user closes their browser tab, the expensive database query is cancelled immediately, freeing up your database connection pool.
Context Propagation Through Layers
A key strength of the context design is how cancellation propagates through your entire call stack automatically. Consider a three-layer architecture:
If the HTTP handler's context is cancelled (client disconnect or timeout), the cancellation propagates down through every layer that received and used that context — the service call stops, the repository query stops, the database driver connection is released.
This tree-based cancellation is why context is the first argument of virtually every meaningful function in production Go code. Libraries like database/sql, net/http, google.golang.org/grpc, and Redis clients all accept context as their first parameter.
Common Mistakes with Context
1. Ignoring ctx.Done()
If your long-running goroutine doesn't listen on ctx.Done(), it will keep running even after the context is cancelled:
2. Storing Context in Structs
The Go team explicitly advises against storing context in a struct field. Context is request-scoped and should be passed explicitly through the call stack, not stored for later use:
Further Reading
Context is the bridge between concurrency and lifecycle management. For the foundational concurrency concepts, see goroutines, channels, and select & WaitGroups. For applying context to real database queries, see Go database connectivity with SQL and GORM.
Best Practices for Using Context Effectively
The context package is simple in API but nuanced in practice. Following these guidelines keeps your concurrent Go code correct and maintainable.
1. Pass context as the first argument, always. Every function that performs I/O, calls an external service, or spawns goroutines should accept a context.Context as its first parameter — named ctx by convention. This is not merely a style preference; it is mandated by the Go documentation at go.dev/doc, which states that context should be the first parameter and never stored in a struct field.
2. Always call the cancel function. Functions like context.WithTimeout and context.WithCancel return both a derived context and a CancelFunc. If you never call the cancel function, Go leaks the internal timer goroutine and the context's resources until the parent is cancelled. Use defer cancel() immediately after the context is created.
3. Set timeouts at the boundary, not deep inside. Apply context.WithTimeout at the outermost call point — the HTTP handler — rather than deep in the repository layer. Inner functions should receive and respect the context they are given. This keeps timeout policy in one place and prevents contradictory timeouts layered on top of each other.
4. Use typed keys for WithValue. Using built-in types (like string) as context keys creates collision risk when two packages store different values under the same key string. Define a private key type per package: type contextKey string. This guarantees uniqueness across packages without coordination.
5. Check ctx.Err() in long loops. If your goroutine runs a loop that does work without any channel operations, it has no way to notice context cancellation unless it explicitly checks ctx.Err() or selects on ctx.Done(). Add a select with a ctx.Done() case at the top of each loop iteration for long-running work.
6. Propagate context to all downstream calls. A context that reaches a database query but not a Redis call means the Redis call continues after the request is cancelled, wasting resources. Pass the same context object to every I/O call in the request path. Libraries like database/sql, net/http, and go-redis all accept context as their first argument, as documented in the Go standard library reference.
7. Use context.TODO() as a clear marker of unfinished work. When you cannot yet thread a context through part of your code, use context.TODO() instead of context.Background(). The names are semantically identical at runtime, but context.TODO() signals to reviewers and tooling that this code still needs to be wired to a real context — making it easy to find with a code search.
8. Avoid leaking goroutines by always listening for ctx.Done(). A goroutine that ignores context cancellation continues running after its caller has moved on, holding database connections and memory indefinitely. Every goroutine that outlives a single function call should have a cancellation mechanism, and ctx.Done() is the standard one.
Next Steps
Now that you've mastered the lifecycle of requests and concurrency, we're ready for the most advanced fundamental module. In the next tutorial, we will explore Reflect and Unsafe, peering under the hood of Go's type system to understand how highly dynamic libraries are built.
Common Context Mistakes in Go
1. Passing context.Background() deep into functions
context.Background() is the root context — use it only at the top level (main, test setup, server start). Pass a derived context (with timeout, deadline, or cancellation) into functions so they can respect cancellation signals.
2. Storing values in context that should be function parameters
context.WithValue is for request-scoped data that crosses API boundaries — request ID, auth token, trace ID. Do not use it as a global variable substitute for ordinary function inputs. The Go blog on context gives authoritative guidance.
3. Not calling the cancel function
ctx, cancel := context.WithTimeout(parent, 5*time.Second) — if you never call cancel(), the resources associated with the context leak until the parent is cancelled. Always defer cancel() immediately after creating a derived context.
4. Ignoring ctx.Done() in long-running loops
A function that loops without checking ctx.Done() will not respond to cancellation. Add a select with case <-ctx.Done(): return ctx.Err() at the top of each iteration for long-running work.
5. Using context values with built-in types as keys
Using context.WithValue(ctx, "key", val) with a string key can collide with keys from other packages. Define a private key type: type ctxKey struct{} and use context.WithValue(ctx, ctxKey{}, val).
Frequently Asked Questions
What is the difference between context.WithTimeout and context.WithDeadline?
WithTimeout(parent, d) cancels the context after duration d from now. WithDeadline(parent, t) cancels at absolute time t. WithTimeout is syntactic sugar for WithDeadline(parent, time.Now().Add(d)). Use WithTimeout for "cancel after N seconds" and WithDeadline when you have a fixed wall-clock deadline.
Should I add context to every function?
Functions that do I/O (HTTP calls, database queries, file reads) should accept a context.Context as the first parameter. Pure computation functions generally do not need context. The Go standard library follows this convention consistently.
How do I propagate context through a goroutine?
Pass the context as a parameter to the goroutine's function — do not store it in a struct or a global. go func(ctx context.Context) { ... }(ctx) is the idiomatic pattern.
