What happens to a request's tracing span when a helper calls context.Background() instead of taking the caller's ctx?
answer
- a fresh root has no parent
- Value walks up the chain it derived from
- nothing the request stored is reachable from there
- not nil-checked, simply absent
- WithoutCancel keeps values; Background keeps nothing
basics
~20 scontext.Background returns an empty root that carries no values, so the span lookup below that helper finds nothing. The work is recorded under no span, or starts a fresh unparented trace, leaving a hole in the request's timeline.
solid answer
~50 sA ctx value lookup walks up the chain of contexts a ctx was derived from. `context.Background()` is a root: it has no parent and holds no values, so anything the request put in its ctx — the span above all — is simply unreachable from there. Nothing panics and nothing errors; the accessor returns nil or a no-op span, so the work below runs fine but is either not recorded at all or recorded as a brand-new root trace. You see it as a gap in the request's span tree exactly where the slow part was, or as orphan traces with no parent. The helper also loses cancellation and the deadline at the same time. The fix is to take `ctx context.Context` as the first parameter and thread it; if the work must genuinely survive cancellation, derive `context.WithoutCancel(ctx)`, which keeps the values.
code
go · 13 linesfunc handle(ctx context.Context) {
ctx = context.WithValue(ctx, spanKey{}, newSpan("handle"))
load(ctx)
}
func load(ctx context.Context) {
report(context.Background()) // fresh root: nothing from the caller survives
}
func report(ctx context.Context) {
sp, ok := ctx.Value(spanKey{}).(*span) // ok is false, sp is nil
_, _ = sp, ok
}go deeper
Remember that context.Background is an empty starting point with nothing in it, so a helper that makes its own cannot see anything the request put in its ctx. Take the caller's ctx as the first parameter instead.
Explain the chain: each derived context references its parent and Value walks upward, so a fresh root ends the walk immediately and returns nil for every key the request had set.
Show how you catch it in practice — reading a trace with a gap where the slow work was, restricting context.Background to entry points in review, and testing that a span is reachable at the deepest layer so a refactor cannot silently empty a dashboard.
Decide the convention across services: which entry points may create a root context, that anything doing I/O takes ctx first, and that work needing to outlive cancellation derives context.WithoutCancel rather than starting a fresh root.
## How a value gets from a handler to a helper A `context.Context` is a linked structure. Every derivation — `context.WithValue`, `context.WithCancel`, `context.WithTimeout` — produces a new context that holds a reference to its parent. `ctx.Value(key)` asks the current context for the key and, if it does not have it, asks its parent, and so on up to the root. That linear walk is the entire mechanism by which a span put in at the top of a request is visible at the bottom. `context.Background()` and `context.TODO()` are roots. They have no parent, no values, no deadline and are never cancelled. The two differ only in what they say to a reader — `TODO` means "a ctx should be plumbed here and is not yet" — and not at all in behaviour. So a helper that builds its own `context.Background()` is not extending the request's chain; it is starting a new one. Any lookup for the request's span walks up a chain of length one and finds nothing. ## Why this is silent Three things conspire to hide it: - **The lookup returns nil, not an error.** `ctx.Value` is typed `any`. Most codebases wrap it in an accessor that either returns `nil, false` via the comma-ok assertion, or returns a usable no-op span so call sites need no nil check. The no-op version is safer and blinder: the code records into a black hole and nobody notices. - **The work still succeeds.** Losing the span costs no functionality. The query runs, the response is written; only the telemetry is wrong. - **The symptom appears far away.** What you actually observe is a trace with a suspicious gap — the handler's span shows 800 ms while its children account for 40 ms — or a pile of short, parentless traces that look like unrelated requests. Neither points at the line of code that dropped the ctx. You also lose the other things the ctx carried at the same moment: cancellation and the deadline. A helper on a fresh `Background` keeps working after the client has gone and after the request's deadline has passed. In a service under load that is the more expensive half of the bug. ## Where the dropped ctx usually comes from - A helper written before ctx was threaded through the package, whose signature nobody wanted to churn. - A method on a struct that has no ctx to hand — often a "manager" or "client" type that was built with dependencies only. - A goroutine started as `go func() { doWork(context.Background()) }()` because the author knew the request ctx would be cancelled and reached for the nearest root instead of a derived one. - A callback whose signature is fixed by an interface that predates ctx, so the implementation invents one. ## Fixing it, and the legitimate uses of Background The fix is boring: give the helper `ctx context.Context` as its first parameter and pass the caller's ctx. Where a callee genuinely must outlive the request's cancellation — flushing or finishing a span after the response is written is the canonical case — the correct tool is `context.WithoutCancel(ctx)` (Go 1.21+). It derives from the request ctx, so `Value` still delegates to the parent chain and the span is still there, but it is never cancelled and has no deadline. `Background` throws the values away as well, which is precisely what you did not want. `Background` remains right in a small set of places: the root of `main`, a test's top-level context, the start of a background loop that has no request behind it at all, and the roots of CLI commands. Everywhere below those it should be regarded as a defect. ## Making it visible Because the compiler cannot help, teams lean on convention and tests: - treat "any function that does I/O, blocks, or starts a span takes ctx first" as a review rule; - restrict `context.Background()` to entry points, and look hard at every other occurrence in review; - write a test that exercises the handler and asserts a span is present at the deepest layer, so a future refactor that drops the ctx fails a test instead of quietly emptying a dashboard; - when reading a span out of a ctx, use the comma-ok form of the type assertion — `sp, ok := ctx.Value(k).(*span)` — so a missing span produces a false rather than a panic, and decide deliberately whether the accessor returns nil or a no-op. ## What a strong answer sounds like "`Background` is a root, so the value chain from the request stops there and the span is unreachable — the work is unparented or unrecorded, and it also stops honouring cancellation and the deadline. Thread the caller's ctx; if the work must survive cancellation, use `WithoutCancel`, which keeps the values."
- How does context.WithoutCancel(ctx) differ from context.Background() for work that must survive the request?`WithoutCancel` derives from ctx, so `Value` still walks up into the request's chain and the span is still found; it just never reports Done and has no deadline. `Background` is an unrelated empty root, so you keep the un-cancellability but throw the span away. Use `WithoutCancel` when the work should stay attributed to the same request.
- Is an accessor that returns a no-op span when none is in the ctx a good idea?It is good for ergonomics — no nil check at every call site, and no panic risk — but it makes this exact bug invisible, because the code happily records into nothing. Pair it with a test that asserts a real span is reachable on the request path, so the silence is caught in CI rather than on a dashboard.
- What is the difference between context.TODO() and context.Background() here?Behaviourally none: both are empty roots with no values, no deadline and no cancellation, so both lose the span. `TODO` is documentation — it marks a place where a real ctx should be plumbed and has not been yet. Neither is an acceptable substitute for the caller's ctx inside a request.
saying these in an interview costs you the question
- Saying context.Background() is fine because the span is global
- Thinking ctx.Value searches all live contexts, not just its own chain
- Claiming a missing span makes the code panic so it would be noticed
- Passing context.TODO() to satisfy the compiler and moving on
- Believing a child context sees values added to its parent afterwards