Last Updated: May 22, 2026
context.Context is Go's standard way to carry cancellation signals, deadlines, and request-scoped values through a call tree. It solves a problem that channels and WaitGroup don't address cleanly: how do you tell a chain of goroutines (and the functions they call) "stop what you're doing, the work isn't needed anymore"? This lesson covers what a Context is, the four methods on the interface, the two root contexts you start from, and the convention of passing it as the first argument everywhere.
Real Go programs are call trees. A handler accepts an HTTP request, calls a function that looks up the customer, which calls another function that hits the database, which kicks off a goroutine to fetch related products. By the time work is in motion, there are five or six functions stacked, plus a handful of goroutines doing parts of the job in parallel.
Now imagine the customer closes their browser. The HTTP server notices the connection drop, but the call tree underneath has no idea. The database query keeps running, the goroutine keeps fetching products, and CPU and memory keep getting spent on a result nobody will ever see. Multiply that by thousands of requests per second and you have a real problem.
context.Context is the fix. It's a single value that flows down the call tree, carrying a "you can stop now" signal. When the top of the tree decides the work is no longer needed (the request was cancelled, a deadline passed, a parent operation failed), every function holding the context can find out and bail out cleanly.
The dotted arrows are the context flowing down. When the client disconnects, the handler cancels its context, and that cancellation propagates to every node downstream. Each function periodically asks "am I still wanted?" and exits if the answer is no. Without a context, every node would keep running until its own work finished.
The package was added to the standard library in Go 1.7 (it lived in golang.org/x/net/context before that). Since then, almost every standard library API that does I/O or starts goroutines has grown a Context-accepting variant: http.Request.Context(), database/sql's QueryContext, net.Dialer.DialContext, and so on. Knowing how to pass and respect a context is non-negotiable in production Go.
context.Context is an interface with four methods. Once you know what each one does, the rest of the package is just helpers that build values satisfying this interface.
Here's what each method gives you:
| Method | Returns | Tells you |
|---|---|---|
Deadline() | time.Time, bool | When this context will be cancelled, or ok == false if no deadline |
Done() | <-chan struct{} | A channel that closes when the context is cancelled |
Err() | error | Why it was cancelled (nil if not yet), one of context.Canceled or context.DeadlineExceeded |
Value(key) | any | A value stored on this context under key, or nil |
The most important method is Done(). It returns a receive-only channel that the context closes when cancellation happens. Receiving from a closed channel returns immediately with the zero value, so a goroutine can use it inside a select to wake up the moment cancellation fires.
Here's the canonical pattern. A worker pulls orders out of a channel and processes them, but it also watches ctx.Done() so it can quit early if asked:
The worker doesn't poll ctx.Done() in a tight loop. It hands the channel to select alongside the work channel, and whichever fires first wins. After cancel() runs, ctx.Done() is closed, the next select picks the <-ctx.Done() case, and the worker returns. We're previewing context.WithCancel here only to make the example complete.
Calling Err() after Done() fires tells you why. There are exactly two values it can return in practice: context.Canceled (somebody called cancel) or context.DeadlineExceeded (the deadline passed). Both are exported sentinel errors, so you can compare with == or use errors.Is:
Deadline() is useful when you want to make a decision based on how much time you have. A function might skip an expensive cache warm-up if it sees there's only 50 milliseconds left, or pick a faster but less accurate algorithm. Most code never calls Deadline() directly; it just trusts that Done() will fire when the deadline hits.
Value() is the one method to be careful with. It carries request-scoped data like a request ID or an auth token down the call tree. Used well, it removes boilerplate. Used badly, it becomes a global variable smuggled through every function.
Cost: ctx.Done() returns the same channel every time you call it, so storing it in a local variable inside a hot loop isn't necessary. But ctx.Value(k) walks up the context chain on each call. If you're reading a value many times in a tight path, pull it out once.
Every context chain has to start somewhere. The standard library gives you two functions that return a "root" context, the trunk from which everything else branches.
context.Background() returns a context that is never cancelled, has no deadline, and carries no values. It's what you use when you're at the top of a call tree and there's no parent context to inherit from. The most common places are main, server startup, test setup, and any long-running background process that owns its own lifetime.
context.TODO() returns the same kind of context, but it's a deliberate signal in code review: "this should accept a context from somewhere, I just haven't wired it through yet." Tools like go vet and many lint configurations flag TODO so you can find every place where the wiring is incomplete.
Functionally identical. The difference is intent. Background says "I genuinely have no parent." TODO says "I'm placeholder code." Both should ideally be called in only a handful of places in a codebase. Once a project has grown, every function below those entry points should accept a context.Context argument and pass it on instead of creating a new root.
Here's how to think about which to pick:
| Use | When |
|---|---|
context.Background() | main, init, top-level goroutines you own, tests, server boot |
context.TODO() | You don't yet know which context to use, want to make that visible |
| Neither | Anywhere a parent context is available, propagate it |
A common mistake is calling context.Background() inside a function that already has a ctx parameter. That breaks the cancellation chain. The function's caller might cancel its context expecting everything downstream to stop, but the new Background context never sees that signal. Always pass through the context you were given.
A Context is immutable once created. You don't modify it, you derive a new one from it. The context package exports four helpers, each of which takes a parent context and returns a child context plus, in most cases, a cancel function:
context.WithCancel(parent) returns a child that can be cancelled by calling the returned cancel function.context.WithTimeout(parent, dur) returns a child that cancels automatically after the duration.context.WithDeadline(parent, time) returns a child that cancels at a specific clock time.context.WithValue(parent, key, val) returns a child that carries a key-value pair.What matters here is the shape: each call takes a parent context and returns a new context that wraps it. The result is a tree.
context.Background() is the root. The HTTP server wraps it with WithCancel to make a per-request context. Some operations under that request need their own deadlines, so they wrap again. One branch adds a request ID with WithValue and the deeper function adds a deadline on top of that. The result is a tree of contexts, each pointing to its parent.
Two rules govern how the tree behaves:
The following program builds a two-level tree and watches both contexts respond when the parent gets cancelled:
We never called cancelChild. The child cancelled because its parent did. This is also why you can't "uncancel" a context; cancellation is one-way, and the only way to get an uncancelled context is to derive a new one from a still-live parent.
Two more facts about the tree:
defer. Even if cancellation also happens through a timeout or a parent, calling cancel immediately when the function returns releases resources the context held. go vet warns when you ignore the returned cancel.WithCancel allocates a small struct and registers a callback with the parent. Building a deep context tree per request is not a performance problem.Cost: Forgetting to call cancel on a WithCancel or WithTimeout context leaks a goroutine inside the context package that watches the parent. The leak is small per request but adds up under load. Always defer cancel().
The Go community has a strict, lint-enforced convention: when a function accepts a context.Context, it's the first parameter, and it's named ctx.
Not the second parameter. Not on the receiver. Not stored in a struct. First parameter, every time, named ctx. This consistency is what makes context propagation visible at a glance. You can scan a function signature and immediately see whether it respects cancellation.
A few related conventions follow from this:
http.Request that exist for the lifetime of one request and explicitly hold a context. For application code, pass the context through function arguments.Options struct. The convention exists so that every caller sees the same shape.The following program follows the convention. The functions are layered (handler calls getCustomer, which calls loadFromDB), each accepts a context, and each passes it on:
Every function in the chain takes ctx as its first parameter. loadFromDB watches ctx.Done() inside a select, so it exits early if the context cancels. getCustomer doesn't do anything with the context beyond forwarding it. Functions in the middle of a call chain don't always introduce new cancellation; they often just propagate whatever they were given.
One more pattern worth knowing: when a function receives a context and wants to add a deadline of its own (say, "this database call should not take longer than 1 second, no matter what"), it derives a new child context from the parent and uses the child for the actual operation:
The parent context (context.Background()) is unbounded. The function adds its own 1-second deadline by deriving childCtx. After 1 second, childCtx.Done() fires and we return context.DeadlineExceeded. The parent isn't touched; if main had wrapped parent in something with a 5-second timeout, that 5-second budget would still be intact. This is how realistic services compose deadlines: each layer adds the tightest constraint it knows about.
A reasonable question is whether every function should accept a Context. The answer is no: a function that does pure arithmetic (no I/O, no blocking, no goroutines) has nothing cancellable, so a context parameter would just be noise. A function that totals a cart in memory doesn't need one. A function that fetches the cart from a database does. The trigger is "I/O, blocking, or goroutines."
When a context cancels, ctx.Err() returns one of exactly two errors. Recognizing them is enough to decide how your function should respond:
WithTimeout or WithDeadline) passed. Same response from your code's point of view, but the cause is different.Both errors satisfy the error interface and are exported as variables, so you compare with errors.Is or, since they're sentinel errors, with ==:
In real code, your callers may wrap ctx.Err() with their own context (for example, fmt.Errorf("loadProduct: %w", err)). That's why errors.Is is the safer comparison: it walks the unwrap chain. Direct == comparisons work for the unwrapped value but break the moment somebody wraps.
When a function returns one of these errors, the calling convention is to propagate it. Don't swallow context.Canceled and return success; the caller asked you to stop, and "I stopped" needs to be visible. Don't return a generic "something went wrong" either; the specific error is what callers branch on.
There's a subtle case worth flagging. The standard library uses context.DeadlineExceeded as the cause when a WithTimeout/WithDeadline context expires, but context.Canceled when the cancel function is called manually. If you set a deadline and also call cancel from a parent for unrelated reasons, the Err() value tells you which one fired first.
A common bug: returning nil after the context cancels, because the function "completed" in the sense that it ran to the end of its select. The fix is the pattern shown above, return ctx.Err() from the cancellation case. Anything else hides cancellation from your caller.
The lifecycle of any context fits the diagram above. It starts active, with Err() returning nil and Done() open. Exactly one transition out is possible: either someone cancels it, or its deadline (if any) is hit. Once it leaves the active state, Err() returns the matching sentinel error and Done() is closed. The transition is one-way; a cancelled context never becomes active again.
A small end-to-end example to tie the pieces together. We'll simulate a checkout that does three things in sequence: verify the cart, charge the card, and send a confirmation. Each step accepts a context and bails out if the context cancels. The top of the program builds a context with a 200-millisecond timeout to keep the whole checkout from running too long.
The three steps take 50, 80, and 40 milliseconds, totaling 170 ms. Comfortably under the 200 ms budget. Try changing the timeout to 100*time.Millisecond and watch the output change:
Now the verify step succeeds, the charge step is partway through when the deadline hits, and chargeCard returns the wrapped DeadlineExceeded. The top-level errors.Is detects it and prints the timeout message.
Notice what we didn't do: we never checked ctx.Err() directly inside checkout. We let the deeper functions check it via their select and propagate the result. The middle layer just chains calls and forwards errors. That's the typical shape: the layers that actually wait on something watch ctx.Done(), and the layers above pass the context through.
This is a complete (if simplified) picture of how context works in practice. You start with a root, derive a child somewhere near the top of the work, pass it down through every function, watch Done() wherever you're about to wait or call I/O, and return the error when it fires. The next chapters cover the four ways to derive children, but the shape stays the same.