skip to content

How do context.Background and context.TODO differ, and how is a context tree built from a root?

level: middleimportance: should knowfreq 58%

answer

  1. a tree needs somewhere to start
  2. two ways to make one from nothing
  3. identical at runtime, different as a message
  4. one of them means unfinished plumbing
  5. cancelling flows to descendants, never to the parent

basics

~20 s

context.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 s

Both 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 lines
go
func 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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