How do context.Background and context.TODO differ, and how is a context tree built from a root?
answer
- a tree needs somewhere to start
- two ways to make one from nothing
- identical at runtime, different as a message
- one of them means unfinished plumbing
- cancelling flows to descendants, never to the parent
basics
~20 scontext.Background and context.TODO behave identically: both are empty roots, never cancelled, with no deadline and no values. TODO only marks unfinished plumbing. Every other context is derived from a root, forming a tree that cancels downward.
solid answer
~50 sBoth are empty roots — never cancelled, no deadline, no values — and they are interchangeable at runtime. The difference is intent: `context.Background()` is what you use at a genuine top level (`main`, package initialisation, a test, or the entry point of an incoming request), while `context.TODO()` marks a place where a real context should be threaded through but the surrounding code has not been updated yet, so it is greppable later. Every other context is *derived*: each `context.With…` function takes a parent and returns a child wrapping it, which builds a tree rooted at that empty root. Cancellation flows strictly downward — cancelling a parent cancels every descendant, while cancelling a child leaves the parent and its siblings alone. The one thing you never do is invent a fresh root partway down a chain: inside an HTTP handler the root is already there, as `r.Context()`.
code
go · 10 linesfunc main() {
ctx := context.Background() // the only root in this program
if err := run(ctx); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
return refresh(ctx) // pass it down; never start a new root here
}go deeper
Know that context.Background is what you write at the top of main or a test, that context.TODO is the placeholder for unfinished plumbing, and that nil is never an acceptable context value.
Be ready to explain that the two roots are behaviourally identical and that every other context is derived, forming a tree where cancellation reaches descendants only. Know that a handler's root is the incoming request's context.
Demonstrate that you hunt for mid-chain context.Background calls in review, since each one silently voids every deadline below it. Be able to say when a detached root is a deliberate choice for work meant to outlive its request.
Decide how the team treats these markers as debt: whether context.TODO is allowed to merge, whether a new root outside main requires a comment justifying it, and how that is enforced in CI rather than by reviewer memory.
## Every context has an ancestor, and the chain ends somewhere Contexts are immutable, so you never modify one — you derive a new one from an existing one. That means every context in a program is a node in a tree, and every tree needs a root. The `context` package provides exactly two ways to make one from nothing. ```go func Background() Context func TODO() Context ``` Both return an empty context: it is never cancelled, it has no deadline, it carries no values, and — a detail worth knowing — its `Done()` method returns a **nil channel**, because it can never be cancelled. ## The difference is documentation, not behaviour At runtime, `Background()` and `TODO()` are indistinguishable in every way that matters to your program. The distinction is a message to the next human. - **`context.Background()`** says "this genuinely is the top." Use it in `main`, in package initialisation, in a test, and as the base of a long-lived background worker's own lifetime. - **`context.TODO()`** says "a real context belongs here and I have not plumbed it through yet." It exists so that the shortcut is *visible*: you can search a codebase for `context.TODO()` and get a list of the places where cancellation is still severed. If you write `context.Background()` for that case instead, the shortcut becomes invisible and permanent, because it now looks like a deliberate root. Both exist for the same reason: **never pass a nil `Context`**, even to a function that would not immediately crash on one. `nil` gives you a panic at the first `ctx.Done()` on a code path nobody exercised in testing; an empty root gives you a call that simply is not cancellable, which is at least honest. ## Deriving the tree Every other context comes from a derivation function, and they all share one shape: *parent in, child out*. ```go ctx := context.Background() // root ctx, cancel := context.WithCancel(ctx) // child of the root defer cancel() // pass ctx onward; anything derived from it is a grandchild ``` The result is a tree. The rules that matter: 1. **Cancellation flows down.** Cancelling a node cancels that node and its entire subtree, transitively. Every affected context's `Done` channel closes. 2. **Cancellation never flows up.** Cancelling a child does nothing to its parent, and nothing to its siblings. A derived context can only *narrow* what its parent already permits. 3. **A derived context is not stronger than its parent.** If the parent is already cancelled when you derive, the child is born cancelled. If the parent carries a deadline, a child cannot extend it — the earliest deadline anywhere in the chain governs. 4. **The child is a wrapper.** It holds a reference to its parent, which is how lookups walk upward and how a cancelled parent knows to close its children. This is why the shape works for a request: one root per request at the top, a subtree per fan-out below it, and one call at the top collapses the whole thing. ## Where a root actually comes from in real code The common mistake is calling `context.Background()` somewhere in the *middle* of a chain, which silently detaches everything below it from the request's cancellation and deadline. In a server there is already a root and it belongs to the request: ```go func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // NOT context.Background() items, err := h.store.List(ctx) // … } ``` `net/http` derives that context for you and cancels it when the client disconnects or the handler returns. Reach for `context.Background()` inside a handler and you have thrown that away: the work carries on after the client is gone, and any deadline the caller established stops applying. There is one legitimate case for a fresh root deep in the codebase — work that is *deliberately* meant to outlive the request that triggered it, such as a background flush. Even then it should be an explicit, commented decision, not an accident of a helper that forgot to take a `ctx` parameter. ## The shape of the convention Put together, the discipline is small and mechanical: one root at the true top of the program or the request; every function in between takes `ctx context.Context` as its first parameter and passes it on; derivations happen where you actually want to narrow the budget; and `context.TODO()` marks anywhere the chain is still broken so that the debt is findable rather than lost.
- A helper deep in a request chain calls context.Background() instead of taking a ctx parameter. What is lost?Everything below that helper is detached from the request. The caller's deadline no longer applies there, so a two-second budget silently becomes unbounded, and when the client disconnects that work carries on holding a connection and memory. It is also invisible in the signature — the helper looks like a plain function, so nobody reviewing the caller can tell the chain was severed.
- If a parent context has a one-second deadline, can a child derived from it get five seconds?No. Derivation can only narrow. A child asking for five seconds still expires when the parent does, because the parent's cancellation propagates to the whole subtree — the earliest deadline anywhere in the chain wins. If a piece of work genuinely must outlive its parent, it needs a context that is not derived from that parent at all, and that should be a deliberate, documented choice.
- Why not just pass nil when a function wants a context and you have nothing meaningful?Because `nil` is a landmine. The context documentation says never to pass one, even if the function tolerates it today: the first layer that calls `ctx.Done()` or `ctx.Err()` on it panics with a nil-pointer dereference, on whatever code path nobody exercised. `context.TODO()` costs the same to type, cannot panic, and leaves a searchable marker.
saying these in an interview costs you the question
- Claims context.TODO cancels itself or carries a default timeout
- Calls context.Background inside an HTTP handler instead of r.Context
- Thinks cancelling a child context also cancels its parent
- Believes a derived context can extend its parent's deadline
- Passes nil rather than an empty root context