Skip to main content

Go context Package: Cancellation, Timeouts, and Values

Published: May 8, 2026 Updated: August 29, 2026 Larry Qu 7 min read

Every non-trivial Go service needs to answer two questions for any in-progress operation: “should I stop?” and “how long do I have?” The context.Context interface provides both, plus a mechanism for carrying request-scoped data through a call chain.

Contexts form a tree. The root is context.Background(). Each With* function creates a child that inherits its parent’s deadline and cancellation, and adds its own constraint. When a parent is cancelled, all its children are cancelled automatically. This propagation is what makes context the right tool for coordinating distributed work — cancel the root request, and everything spawned from it stops.

For the goroutine patterns that use context see Go goroutines and Go worker pools.

The Four Context Constructors

context.WithCancel — Manual Cancellation

WithCancel returns a child context and a cancel function. Call cancel() to stop all goroutines watching the child:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()  // always defer — prevents context leak if you forget to call it

go func() {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("stopping:", ctx.Err()) // context.Canceled
            return
        default:
            doWork()
        }
    }
}()

time.Sleep(500 * time.Millisecond)
cancel()  // signals all goroutines watching this context to stop

defer cancel() is idiomatic even when you call cancel() explicitly — it ensures the cancellation fires even if the function returns early through an error.

context.WithTimeout — Relative Deadline

WithTimeout cancels the context after a duration. Use it for operations that must complete within a budget:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

resp, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)

WithTimeout(parent, d) is exactly equivalent to WithDeadline(parent, time.Now().Add(d)). Use WithTimeout when thinking in relative durations; use WithDeadline when you have an absolute deadline from an upstream caller.

context.WithDeadline — Absolute Deadline

deadline := time.Now().Add(30 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

// Check how much time remains
remaining, _ := ctx.Deadline()
fmt.Println("deadline in:", time.Until(remaining).Round(time.Second))

When a child context has a shorter deadline than the parent, the child’s deadline applies. When the parent’s deadline is shorter, the parent’s applies — you can never extend a deadline by creating a child context.

context.WithValue — Request-Scoped Data

WithValue attaches a key-value pair to a context. Values propagate to all child contexts. Use it only for request-scoped metadata (trace IDs, authenticated user, request ID) — not for optional function parameters:

type contextKey struct{ name string }

var (
    requestIDKey = contextKey{"requestID"}
    userIDKey    = contextKey{"userID"}
)

// Set in middleware
func requestMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := generateRequestID()
        ctx := context.WithValue(r.Context(), requestIDKey, id)
        w.Header().Set("X-Request-ID", id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Read anywhere in the call chain
func logEvent(ctx context.Context, msg string) {
    reqID, _ := ctx.Value(requestIDKey).(string)
    slog.Info(msg, slog.String("request_id", reqID))
}

Use a private unexported type for the key (the contextKey struct above) to prevent collisions with keys from other packages. Using a plain string like context.WithValue(ctx, "userID", id) risks a collision with any other package that uses the same string.

Propagating Context Through a Call Chain

Context is always the first argument, always named ctx. Every function that does I/O, blocks, or spawns goroutines should accept and pass it:

func handleOrder(ctx context.Context, orderID string) (*Order, error) {
    // Pass context to every downstream call
    user, err := getUser(ctx, orderID)
    if err != nil {
        return nil, err
    }
    inventory, err := checkInventory(ctx, orderID)
    if err != nil {
        return nil, err
    }
    return processOrder(ctx, user, inventory)
}

func getUser(ctx context.Context, orderID string) (*User, error) {
    // ctx propagates — if handleOrder's context is cancelled, this returns immediately
    return db.QueryContext(ctx, "SELECT * FROM users JOIN orders ...")
}

When the context is cancelled — because the HTTP request timed out, the client disconnected, or the operator cancelled the job — db.QueryContext returns immediately with ctx.Err(). No manual polling, no additional channels needed.

HTTP: Context Flows from Request to Response

The Go HTTP server creates a context per request, accessible via r.Context(). It’s automatically cancelled when the client disconnects. Always use this context for downstream calls:

func userHandler(w http.ResponseWriter, r *http.Request) {
    // r.Context() is already cancelled if the client disconnects
    user, err := db.QueryRowContext(r.Context(),
        "SELECT id, name FROM users WHERE id = $1",
        r.PathValue("id"),
    ).Scan(&u.ID, &u.Name)

    if errors.Is(err, context.Canceled) {
        // Client disconnected — no need to send a response
        return
    }
    if err != nil {
        http.Error(w, "database error", http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(user)
}

For outgoing HTTP client calls, attach the context to the request with http.NewRequestWithContext:

func callDownstream(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("calling %s: %w", url, err)
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

If the upstream request context is cancelled while the downstream call is in flight, Do returns immediately with a context error.

Checking Cancellation in Long-Running Loops

Any function that loops over work items should check the context periodically:

func processItems(ctx context.Context, items []Item) error {
    for i, item := range items {
        // Check before each item, not just at the start
        if err := ctx.Err(); err != nil {
            return fmt.Errorf("cancelled after %d items: %w", i, err)
        }
        if err := process(ctx, item); err != nil {
            return fmt.Errorf("item %d: %w", i, err)
        }
    }
    return nil
}

ctx.Err() returns nil when the context is still active, context.Canceled when cancelled, and context.DeadlineExceeded when the deadline passed. Checking it at the top of each iteration lets you stop promptly rather than processing thousands of items after the deadline.

Graceful Shutdown with Context

Server shutdown is another key use case. Listen for OS signals, cancel a root context, and let all in-flight operations respect the cancellation:

func main() {
    rootCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    srv := &http.Server{
        Addr:    ":8080",
        Handler: buildRouter(rootCtx),
    }

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("server: %v", err)
        }
    }()

    <-rootCtx.Done()
    log.Println("shutdown signal received")

    shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Printf("shutdown error: %v", err)
    }
}

signal.NotifyContext (Go 1.16+) creates a context that cancels when the OS signal arrives. The 30-second shutdown context gives in-flight requests time to complete before the process exits.

Context Rules

Never store a context in a struct. Pass it as a parameter. Storing a context means it can outlive its intended scope and prevents GC:

// ❌ Don't store context in a struct
type Service struct {
    ctx context.Context  // wrong
}

// ✅ Pass it as a parameter
func (s *Service) DoWork(ctx context.Context) error { ... }

context.Background() at the top, context.TODO() as a placeholder. Background is for main functions, test setup, and the root of a request tree. TODO signals “I’ll add proper context propagation later” — it’s a marker for incomplete code, caught by go vet in some linters.

Always defer cancel(). Every With* function returns a cancel function. Not calling it leaks the context’s goroutine until the parent is cancelled. defer cancel() ensures cleanup even if you return early.

Diagnosing Context Errors

if err != nil {
    switch {
    case errors.Is(err, context.Canceled):
        // Request was explicitly cancelled — usually client disconnect or manual cancel
    case errors.Is(err, context.DeadlineExceeded):
        // Timed out — log the deadline and consider increasing it or optimizing the operation
    default:
        // Real error — log and handle
    }
}

errors.Is works through wrapped errors, so fmt.Errorf("query: %w", ctx.Err()) still matches errors.Is(err, context.DeadlineExceeded).

Summary

  • WithCancel: manual stop — call cancel() when you’re done, always defer cancel()
  • WithTimeout / WithDeadline: automatic stop after duration or at time — the shorter deadline always wins between parent and child
  • WithValue: request metadata (trace IDs, auth) — use private struct types as keys to avoid collisions
  • Pass ctx context.Context as the first argument to every function that does I/O, blocks, or launches goroutines
  • Use r.Context() in HTTP handlers and http.NewRequestWithContext in clients — context flows naturally through the request lifecycle
  • Check ctx.Err() in loops; use errors.Is(err, context.DeadlineExceeded) to distinguish timeout from cancellation

Resources

Comments

👍 Was this article helpful?