AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 14 — context — Deadlines, Cancellation, and Request-Scoped Values

14 — context — Deadlines, Cancellation, and Request-Scoped Values

August 13, 20265 min read
Download as Markdown

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

[2] The Go Authors, "Canceling in-progress operations," Go Documentation, 2024. [Online]. Available: 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

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

Knowledge check · Question 1 of 5

Why is context.Context passed as the first parameter of so many Go functions?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!