Go Context Package: Controlling Cancellation and Deadlines

The Request That Never Ends

A user sends a request to your API. Your service calls a database, which calls another service, which calls another. Somewhere deep in this chain, something hangs. The user gave up and closed their browser 30 seconds ago. But your services keep working, waiting, consuming resources for a response that nobody wants.
Without context, there's no way to tell downstream operations "stop, nobody cares anymore." They run until completion or timeout, wasting resources and potentially causing cascading failures.

Why Context Exists

Context solves the problem of request-scoped data and cancellation across API boundaries. It answers:
  • When should this operation stop?
  • What deadline is this operation working against?
  • What request-specific data should be available?
Go blog diagram 1

Go blog diagram 1

The Radio Broadcast Analogy

Think of it like this: Context is like a radio broadcast to all workers on a construction site. The foreman can announce "stop work" and everyone hears it instantly. Each worker checks their radio periodically. When they hear "stop," they clean up and quit. Without the radio, the foreman would have to run to each worker individually. Some might never get the message.

Creating and Using Context

The Root Context

Every context chain starts from context.Background() or context.TODO().
go
// Filename: context_basics.go package main import ( "context" "fmt" ) func main() { // Background: the root context, never cancelled ctx := context.Background() fmt.Println("Background context:", ctx) // TODO: placeholder when unsure which context to use ctx2 := context.TODO() fmt.Println("TODO context:", ctx2) }

Context with Cancellation

go
// Filename: context_cancel.go package main import ( "context" "fmt" "time" ) func worker(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s: received cancel signal, stopping\n", name) return default: fmt.Printf("%s: working...\n", name) time.Sleep(500 * time.Millisecond) } } } func main() { // Create cancellable context ctx, cancel := context.WithCancel(context.Background()) // Start workers go worker(ctx, "Worker-1") go worker(ctx, "Worker-2") // Let them work for 2 seconds time.Sleep(2 * time.Second) // Cancel all workers fmt.Println("Main: cancelling all workers") cancel() // Wait for workers to finish time.Sleep(500 * time.Millisecond) fmt.Println("Main: done") }
Expected Output:
Worker-1: working... Worker-2: working... Worker-1: working... Worker-2: working... Worker-1: working... Worker-2: working... Worker-1: working... Worker-2: working... Main: cancelling all workers Worker-2: received cancel signal, stopping Worker-1: received cancel signal, stopping Main: done
Go blog diagram 2

Go blog diagram 2

Context with Timeout

go
// Filename: context_timeout.go package main import ( "context" "fmt" "time" ) func slowOperation(ctx context.Context) error { select { case <-time.After(5 * time.Second): return nil // Operation completed case <-ctx.Done(): return ctx.Err() // Cancelled or timed out } } func main() { // Context that cancels after 2 seconds ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() // Always call cancel to release resources fmt.Println("Starting slow operation...") err := slowOperation(ctx) if err != nil { fmt.Println("Operation failed:", err) } else { fmt.Println("Operation succeeded") } }
Expected Output:
Starting slow operation... Operation failed: context deadline exceeded

Context with Deadline

go
// Filename: context_deadline.go package main import ( "context" "fmt" "time" ) func main() { // Set absolute deadline deadline := time.Now().Add(3 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() // Check the deadline if d, ok := ctx.Deadline(); ok { fmt.Printf("Deadline: %v\n", d) fmt.Printf("Time until deadline: %v\n", time.Until(d)) } // Wait for deadline <-ctx.Done() fmt.Println("Deadline reached:", ctx.Err()) }
Expected Output:
Deadline: 2024-01-28 10:00:03 +0000 UTC Time until deadline: 2.999s Deadline reached: context deadline exceeded

Context Values: Request Scoped Data

Context can carry request-scoped values. Use this sparingly for data like request IDs, not for passing function parameters.
go
// Filename: context_values.go package main import ( "context" "fmt" ) // Define custom types for context keys to avoid collisions type contextKey string const ( requestIDKey contextKey = "requestID" userIDKey contextKey = "userID" ) func handleRequest(ctx context.Context) { // Retrieve values requestID := ctx.Value(requestIDKey) userID := ctx.Value(userIDKey) fmt.Printf("Handling request %v for user %v\n", requestID, userID) // Call downstream with same context processData(ctx) } func processData(ctx context.Context) { requestID := ctx.Value(requestIDKey) fmt.Printf("Processing data for request %v\n", requestID) } func main() { // Create context with values ctx := context.Background() ctx = context.WithValue(ctx, requestIDKey, "req-12345") ctx = context.WithValue(ctx, userIDKey, 42) handleRequest(ctx) }
Expected Output:
Handling request req-12345 for user 42 Processing data for request req-12345

Real World Example: HTTP Handler with Timeout

go
// Filename: http_context.go package main import ( "context" "fmt" "net/http" "time" ) func fetchFromDatabase(ctx context.Context, query string) (string, error) { // Simulate slow database query select { case <-time.After(2 * time.Second): return "data from db", nil case <-ctx.Done(): return "", ctx.Err() } } func fetchFromAPI(ctx context.Context, url string) (string, error) { // Simulate slow API call select { case <-time.After(1 * time.Second): return "data from api", nil case <-ctx.Done(): return "", ctx.Err() } } func handler(w http.ResponseWriter, r *http.Request) { // Request context is cancelled when client disconnects ctx := r.Context() // Add timeout ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() // Parallel calls with shared context type result struct { data string err error } dbCh := make(chan result, 1) apiCh := make(chan result, 1) go func() { data, err := fetchFromDatabase(ctx, "SELECT * FROM users") dbCh <- result{data, err} }() go func() { data, err := fetchFromAPI(ctx, "http://api.example.com/data") apiCh <- result{data, err} }() // Collect results var dbResult, apiResult result for i := 0; i < 2; i++ { select { case dbResult = <-dbCh: case apiResult = <-apiCh: case <-ctx.Done(): http.Error(w, "Request timeout", http.StatusGatewayTimeout) return } } if dbResult.err != nil || apiResult.err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } fmt.Fprintf(w, "DB: %s, API: %s", dbResult.data, apiResult.data) } func main() { http.HandleFunc("/", handler) fmt.Println("Server starting on :8080") http.ListenAndServe(":8080", nil) }

Context Propagation Pattern

Go blog diagram 3

Go blog diagram 3

Best Practices

Always Pass Context First

go
// RIGHT: Context is first parameter func FetchUser(ctx context.Context, id int) (*User, error) { // ... } // WRONG: Context buried in parameters func FetchUser(id int, ctx context.Context) (*User, error) { // ... }

Always Call Cancel

go
// RIGHT: defer cancel immediately ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second) defer cancel() // Resources released even if we return early

Check Done in Loops

go
func process(ctx context.Context, items []Item) error { for _, item := range items { // Check before each iteration select { case <-ctx.Done(): return ctx.Err() default: } if err := processItem(item); err != nil { return err } } return nil }

Use Values Sparingly

go
// WRONG: Using context for function parameters ctx = context.WithValue(ctx, "username", "alice") ctx = context.WithValue(ctx, "password", "secret") // Never! // RIGHT: Only for request-scoped metadata ctx = context.WithValue(ctx, requestIDKey, uuid.New().String()) ctx = context.WithValue(ctx, traceIDKey, traceID)

Common Mistakes

Mistake 1: Ignoring context in long operations
go
// WRONG: Doesn't respect context func longOperation(ctx context.Context) { time.Sleep(10 * time.Second) // Ignores cancellation } // RIGHT: Checks context func longOperation(ctx context.Context) error { select { case <-time.After(10 * time.Second): return nil case <-ctx.Done(): return ctx.Err() } }
Mistake 2: Creating new background context
go
// WRONG: Breaks cancellation chain func handler(ctx context.Context) { newCtx := context.Background() // Parent cancellation ignored! callDownstream(newCtx) } // RIGHT: Derive from parent func handler(ctx context.Context) { childCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() callDownstream(childCtx) }
Mistake 3: Storing context in struct
go
// WRONG: Context stored in struct type Service struct { ctx context.Context // Don't do this } // RIGHT: Pass context to methods type Service struct{} func (s *Service) Process(ctx context.Context) error { // Use ctx here }

Context Decision Tree

Go blog diagram 4

Go blog diagram 4

Context Comparison

FunctionUse CaseCancellation
Background()Root context for main, initNever
TODO()Placeholder during developmentNever
WithCancel()Manual cancellation controlOn cancel() call
WithTimeout()Operation time limitAfter duration
WithDeadline()Absolute time limitAt deadline time
WithValue()Pass request-scoped dataInherits from parent

What You Learned

You now understand that:
  • Context enables cancellation: Signal all operations to stop
  • Timeout and deadline limit operations: Prevent runaway work
  • Values carry request metadata: Not for function parameters
  • Always propagate context: Don't break the chain
  • Check Done regularly: In loops and long operations
  • Always call cancel: Release resources

Your Next Steps

  • Audit: Add context to functions that do I/O or may block
  • Read Next: Learn about goroutine leaks and how context prevents them
  • Practice: Add request tracing using context values
Context is Go's answer to request-scoped work. It prevents resource waste, enables timeouts, and carries metadata. When you master context, your applications become responsive and resource-efficient. Every operation knows when to stop.
All Blogs
Tags:golangcontextcancellationtimeoutsconcurrency