When an incoming HTTP request is cancelled by the client, every downstream database query and RPC call associated with that request must stop immediately. Go’s
context.Context enforces this cancellation propagation.TL;DR (Quick Summary)#
context.Context: Standard interface carrying cancellation signals, deadlines, and request metadata.- Golden Rule: Always pass
ctx context.Contextas the first parameter of a function (func DoWork(ctx context.Context, ...)). WithTimeout(parent, duration): Automatically sends a cancellation signal viactx.Done()when the deadline expires.WithCancel(parent): Manual cancellation trigger (cancel()).WithValue(parent, key, val): Attaches immutable request-scoped metadata (e.g. Request ID, Auth Token).
1. Context Cancellation Tree Propagation#
graph TD
subgraph ContextTree ["Context Tree"]
RootContext["context.Background()"] --> RequestCtx["context.WithTimeout(Root, 2s)"]
RequestCtx --> ServiceCtx["Service Context"]
ServiceCtx --> QueryCtx["Database Context"]
ServiceCtx --> APICtx["HTTP Client Context"]
end
TimeoutSignal["2s Timeout Expired"] -->|Cancels| RequestCtx
RequestCtx -->|Propagates Cancellation Downward| ServiceCtx
ServiceCtx -->|Cancels Active Work| QueryCtx
ServiceCtx -->|Cancels Active Work| APICtx
2. Step-by-Step Lab: Implementing HTTP Client & Database Timeouts#
package main
import (
"context"
"fmt"
"time"
)
// Simulate a slow database query
func queryDatabase(ctx context.Context) (string, error) {
select {
case <-time.After(500 * time.Millisecond): // Takes 500ms
return "Query Result: User Record 101", nil
case <-ctx.Done(): // Triggered if timeout expires early!
return "", ctx.Err() // Returns context.DeadlineExceeded or Canceled
}
}
func main() {
// Scenario 1: Timeout shorter than query time (Triggers Cancellation)
fmt.Println("=== Scenario 1: Short Timeout (100ms) ===")
ctx1, cancel1 := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel1()
res1, err1 := queryDatabase(ctx1)
if err1 != nil {
fmt.Println("Operation Aborted:", err1) // context.DeadlineExceeded
} else {
fmt.Println("Success:", res1)
}
// Scenario 2: Timeout longer than query time (Succeeds)
fmt.Println("\n=== Scenario 2: Sufficient Timeout (1s) ===")
ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel2()
res2, err2 := queryDatabase(ctx2)
if err2 != nil {
fmt.Println("Operation Aborted:", err2)
} else {
fmt.Println("Success:", res2)
}
}Expected Terminal Output:
=== Scenario 1: Short Timeout (100ms) ===
Operation Aborted: context deadline exceeded
=== Scenario 2: Sufficient Timeout (1s) ===
Success: Query Result: User Record 1013. Comparison: Context Constructors#
| Constructor | Behavior | When to Use |
|---|---|---|
context.Background() | Non-nil, empty root context. | Top-level entry points (main(), request handlers). |
WithTimeout(parent, duration) | Cancels automatically when duration elapses. | Database queries, external HTTP calls. |
WithCancel(parent) | Cancels manually when cancel() is called. | Aborting background worker Goroutines. |
WithValue(parent, key, val) | Stores immutable key-value metadata. | Passing Request IDs, Trace IDs, or Auth Claims. |
4. Troubleshooting & Common Errors#
Error 1: Forgetting to Call cancel()#
The Cause: WithTimeout or WithCancel allocates resources in the context tree. Failing to call cancel() leaks memory and timers until the parent context expires!
The Fix: Always defer cancel() immediately after creating a context:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // 🟢 Guarantees cleanup when function exits!Summary & Next Steps#
In this episode:
- We passed
context.Contextacross function boundaries. - We set strict execution timeouts using
context.WithTimeout. - We observed downward cancellation propagation.
In Episode 13: Generics in Go, we will master Type Parameters [T any] and type constraints!

