Context Patterns for Production Go Code
Learn production-grade context patterns in Go including timeouts, cancellation propagation, value passing, and middleware integration.
What you'll learn
- ✓How to propagate cancellation through call chains
- ✓Timeout and deadline patterns for HTTP and database calls
- ✓When and how to store values in context safely
Prerequisites
- •Basic Go knowledge
- •Familiarity with goroutines and interfaces
The context package is the backbone of production Go code. Every server handler, every database query, every outbound HTTP call should accept a context.Context as its first parameter. This guide covers the patterns you will use daily.
Why Context Matters
A single API request can spawn database queries, cache lookups, and calls to downstream services. If the client disconnects, you want all of that work to stop immediately. Context makes this possible by providing a cancellation signal that propagates through your entire call chain.
func HandleOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // carries the client's cancellation signal
order, err := fetchOrder(ctx, orderID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
enriched, err := enrichWithInventory(ctx, order)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(enriched)
}
If the client disconnects mid-request, ctx.Done() closes, and any well-behaved downstream function returns early.
Timeouts and Deadlines
context.WithTimeout
Use WithTimeout when you want to cap how long an operation takes.
func fetchOrder(ctx context.Context, id string) (*Order, error) {
// Give the database query at most 3 seconds
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() // always call cancel to release resources
row := db.QueryRowContext(ctx, "SELECT id, total FROM orders WHERE id = $1", id)
var order Order
if err := row.Scan(&order.ID, &order.Total); err != nil {
return nil, fmt.Errorf("fetch order %s: %w", id, err)
}
return &order, nil
}
The defer cancel() call is critical. Without it, the context’s internal timer goroutine leaks until the parent context is cancelled.
context.WithDeadline
Use WithDeadline when you have an absolute wall-clock deadline, such as an SLA that says “respond by T”.
func HandleWithSLA(w http.ResponseWriter, r *http.Request) {
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(r.Context(), deadline)
defer cancel()
result, err := doExpensiveWork(ctx)
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timed out", http.StatusGatewayTimeout)
return
}
json.NewEncoder(w).Encode(result)
}
Nested Timeouts
When you nest timeouts, the shortest one wins. This is by design.
func processRequest(ctx context.Context) error {
// Parent gives us 10 seconds total
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// Step 1 gets at most 3 seconds
step1Ctx, step1Cancel := context.WithTimeout(ctx, 3*time.Second)
defer step1Cancel()
if err := stepOne(step1Ctx); err != nil {
return fmt.Errorf("step 1: %w", err)
}
// Step 2 gets at most 5 seconds, but also bounded by parent's remaining time
step2Ctx, step2Cancel := context.WithTimeout(ctx, 5*time.Second)
defer step2Cancel()
if err := stepTwo(step2Ctx); err != nil {
return fmt.Errorf("step 2: %w", err)
}
return nil
}
Cancellation Propagation
context.WithCancel
Use WithCancel when you launch background work that should stop when the caller is done.
func streamResults(ctx context.Context, query string) (<-chan Result, error) {
ctx, cancel := context.WithCancel(ctx)
ch := make(chan Result)
go func() {
defer cancel()
defer close(ch)
rows, err := db.QueryContext(ctx, query)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var r Result
if err := rows.Scan(&r.ID, &r.Value); err != nil {
return
}
select {
case ch <- r:
case <-ctx.Done():
return
}
}
}()
return ch, nil
}
context.WithCancelCause (Go 1.20+)
When you cancel a context, you often want to explain why. WithCancelCause lets you attach an error.
func worker(ctx context.Context) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
go func() {
if err := healthCheck(); err != nil {
cancel(fmt.Errorf("health check failed: %w", err))
}
}()
select {
case <-ctx.Done():
return context.Cause(ctx) // returns the error passed to cancel
case result := <-doWork(ctx):
return processResult(result)
}
}
Context Values: Use Sparingly
Context values are for request-scoped data that crosses API boundaries, not for passing function parameters.
Good Uses for Context Values
// Define unexported key types to avoid collisions
type contextKey string
const (
requestIDKey contextKey = "requestID"
userIDKey contextKey = "userID"
)
// Provide typed accessors
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func RequestIDFrom(ctx context.Context) (string, bool) {
id, ok := ctx.Value(requestIDKey).(string)
return id, ok
}
Middleware That Sets Context Values
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.NewString()
}
ctx := WithRequestID(r.Context(), id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Anti-Patterns to Avoid
Do not use context values for:
- Database connections (pass them as struct fields)
- Logger instances (embed them in a struct or use slog’s handler)
- Configuration (use dependency injection)
The rule of thumb: if removing the value from context would break your program’s correctness (not just its observability), it probably should not be in context.
Production Pattern: Graceful Shutdown
Combine context.WithCancel with os/signal for clean server shutdown.
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
<-ctx.Done()
log.Println("shutting down gracefully...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("shutdown error: %v", err)
}
log.Println("server stopped")
}
Notice that Shutdown uses a fresh context.Background() with its own timeout, not the cancelled parent context.
Checking Context Before Expensive Work
In long-running loops, check the context regularly.
func processBatch(ctx context.Context, items []Item) error {
for i, item := range items {
select {
case <-ctx.Done():
return fmt.Errorf("cancelled after %d/%d items: %w", i, len(items), ctx.Err())
default:
}
if err := processItem(ctx, item); err != nil {
return fmt.Errorf("item %d: %w", i, err)
}
}
return nil
}
The select with a default case is non-blocking. It checks whether the context has been cancelled without pausing.
Summary
- Always pass
context.Contextas the first parameter. - Always
defer cancel()after creating a derived context. - Use
WithTimeoutfor duration-based limits andWithDeadlinefor absolute limits. - Use
WithCancelCauseto attach reasons to cancellations. - Keep context values minimal: request IDs, trace IDs, and auth metadata only.
- Check
ctx.Done()inside long-running loops. - Use
signal.NotifyContextfor graceful shutdown.
These patterns will keep your production services responsive, observable, and well-behaved under load.
Related articles
- Go Go Context Cancellation Patterns
Master Go's context package: propagate deadlines, cancel goroutines safely, and avoid leaks with practical patterns for HTTP, database, and pipeline code.
- Go Go context Package Explained
How to use Go's context package effectively: cancellation, deadlines, propagation, request-scoped values, and the patterns that keep services responsive.
- Go Go Concurrency: Fan-Out, Pipeline, and Worker Pool
Master advanced Go concurrency patterns including fan-out/fan-in, pipelines, and worker pools with practical examples and production-ready code.
- Go Go Database Patterns with sqlx and pgx
Master Go database patterns using sqlx and pgx for PostgreSQL including connection pooling, transactions, batch operations, and repository design.