---
title: "14 — context — Deadlines, Cancellation, and Request-Scoped Values"
uid: context-package
tags: ["golang", "context", "cancellation", "concurrency", "timeouts", "roadmap:golang", "deadlines", "request-scoped"]
excerpt: "A context.Context is the first parameter of every I/O function, carrying a deadline, a cancellation signal, and request-scoped values down the call stack. The boilerplate is the point."
date: 2026-08-13T03:28:10+0000
source: https://www.aveshina.my.id/en/blog/context-package
---

Why does every Go function take a context.Context as its first parameter? That question made the package look like boilerplate until I saw what it carries. The model that clicked: **a context.Context is a value passed as the first parameter of every function that might do I/O, and it carries three things down the call stack — a deadline, a cancellation signal, and request-scoped values — so that when a client disconnects or a timeout fires, the entire subtree of work shuts down cleanly.** [1] The boilerplate is the point: a uniform parameter that every layer knows to honor.

## What a context carries

A Context is an interface that threads three concerns across API and process boundaries [1]:

- **Deadline** — an absolute time by which the work should be abandoned.
- **Cancellation signal** — a Done() channel that closes when the work should stop.
- **Request-scoped values** — typed key/value pairs for data that should travel with a specific request (a trace ID, a request ID, an auth principal).

The first two are the load-bearing parts. They let a server, mid-way through a slow database query, learn that the HTTP client has already disconnected — and stop the query instead of finishing work nobody will receive. Without context, that query would run to completion, waste resources, and log a confusing "client gone" error at the end.

## The tree — contexts form a hierarchy

Contexts are not created standalone; they derive from a parent and form a tree [1]. The two roots are context.Background() (the empty context, used at the top of main and in tests) and context.TODO() (a placeholder when you have not decided yet). From a parent, you derive children:

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

ctx, cancel := context.WithCancel(parent)
defer cancel()
```

The crucial property: **when a parent is cancelled or times out, all of its children are cancelled too.** [2] A request handler creates a timeout context from the server's root; every database call, HTTP client call, and goroutine spawned by the handler derives from that timeout context; when the timeout fires, the entire subtree receives the cancellation signal at once. This is how Go prevents the "zombie goroutine" leak where background work outlives the request that started it.

## Deadlines and cancellations in practice

The mechanism inside every derived context is the Done() channel. When the deadline fires or cancel() is called, Done() closes [2]. Idiomatic functions check it [4]:

```
func fetchUser(ctx context.Context, id int) (*User, error) {
    row := db.QueryRowContext(ctx, "SELECT ... WHERE id = $1", id)
    // the database driver honors ctx — if cancelled, the query aborts
    var u User
    err := row.Scan(&u.Name)
    if err != nil {
        return nil, fmt.Errorf("fetchUser: %w", err)
    }
    return &u, nil
}
```

The standard library's I/O packages all accept a context-aware variant: QueryRowContext, http.NewRequestWithContext, os.OpenFile (via wrappers). These check ctx.Done() internally and abort the operation when the context is cancelled. The rule for any function you write that does I/O or long computation: take a context.Context as its first parameter and pass it to every downstream call [2].

A subtle but essential discipline: **call the cancel function.** WithTimeout and WithCancel return a cancel function that releases resources and removes the child from the parent. Even if the timeout fires on its own, you must call cancel — the standard idiom is defer cancel() immediately after creation. The linters and go vet will flag a missing cancel call.

## Common use cases

The recurring patterns where context earns its keep [3]:

- **HTTP request timeouts.** The server wraps each incoming request in a context that is cancelled when the client disconnects. Handlers that honor it stop work the moment the client goes away.
- **Database deadlines.** Pass a timeout context to QueryContext so a slow query fails fast instead of holding a connection for minutes.
- **Goroutine coordination.** A long-running background worker accepts a context so the caller can shut it down cleanly on shutdown or config reload.
- **Distributed tracing.** A trace ID stored as a request-scoped value travels with every downstream call, so logs across services can be correlated.

Request-scoped values (context.WithValue) are the most-misused part. They are for data that must cross API boundaries and cannot live in a function parameter — a trace ID, a tenant ID extracted from auth. They are _not_ for passing function arguments that you were too lazy to add to the signature. Values are untyped (any), so they bypass the type system, and overusing them makes code opaque. The rule: if you can pass it as a parameter, pass it as a parameter; reserve WithValue for cross-cutting concerns.

## How I use this

The context.Context-as-first-parameter habit is now automatic. Every function that does I/O takes one, passes it to every downstream call, and the whole subtree becomes cancellable. In HTTP servers I set a per-request timeout derived from the server's root context, so a disconnected client stops the work, not just the response. And I treat the cancel function as mandatory: ctx, cancel := context.WithTimeout(...); defer cancel() is a single motion, never split. The request-scoped value slot I use sparingly — almost exclusively for trace IDs — because the moment a value starts being read in multiple places, a real parameter would be clearer. The payoff is production stability: no leaked goroutines, no zombie queries, no work done for clients that have already gone.

## References

[1] S. Cox, "Go Concurrency Patterns: Context," The Go Blog, 2014. [Online]. Available: [https://go.dev/blog/context](https://go.dev/blog/context)

[2] The Go Authors, "Canceling in-progress operations," Go Documentation, 2024. [Online]. Available: [https://go.dev/doc/database/cancel-operations](https://go.dev/doc/database/cancel-operations)

[3] J. Kaksouri, "The Complete Guide to Context in Golang," Medium, 2024. [Online]. Available: [https://medium.com/@jamal.kaksouri/the-complete-guide-to-context-in-golang-efficient-concurrency-management-43d722f6eaea](https://medium.com/@jamal.kaksouri/the-complete-guide-to-context-in-golang-efficient-concurrency-management-43d722f6eaea)

[4] WebDevStation, "Understanding Golang Context: Cancellation, Timeouts," 2024. [Online]. Available: [https://webdevstation.com/posts/understanding-golang-context/](https://webdevstation.com/posts/understanding-golang-context/)

```quiz
Q: Why is context.Context passed as the first parameter of so many Go functions?
- It is required by the language for every function
- It carries deadlines, cancellation signals, and request-scoped values, so any function that does I/O should honor it and pass it downstream
correct: 1
explain: Context is the standard channel for cancellation and deadlines. Making it the first parameter of I/O functions means every layer of a call stack can be told to stop when a client disconnects or a timeout fires.

Q: When a parent context is cancelled, what happens to its children?
- Nothing — each context is independent
- All children derived from it are cancelled too, propagating the signal down the subtree
correct: 1
explain: Contexts form a tree. Cancellation flows from parent to all descendants, so cancelling a request's context stops every database call, HTTP request, and goroutine spawned by that request.

Q: Why must you call the cancel function returned by WithTimeout/WithCancel, even if the timeout already fired?
- You do not have to — the timeout firing is enough
- Calling cancel releases resources and removes the child from the parent; the linters flag a missing call, and the leak accumulates over time
correct: 1
explain: The cancel function cleans up the child's bookkeeping. The standard idiom is `defer cancel()` right after creation, regardless of whether the deadline fires naturally.

Q: context.WithValue is appropriate for…
- passing any function argument you did not want to add to the signature
- cross-cutting concerns like a trace ID that must cross API boundaries and cannot be a normal parameter
correct: 1
explain: WithValue bypasses the type system and makes code opaque. It is reserved for request-scoped data like trace IDs; everything else should be a real parameter.

Q: A database query using QueryRowContext(ctx, ...) will…
- ignore ctx and run to completion regardless
- abort early when ctx is cancelled or its deadline fires, freeing the connection
correct: 1
explain: The database driver honors the context. When Done() closes, the query is cancelled mid-flight, which is exactly why context-aware query variants exist.
```
