Right, let’s talk about the two most misunderstood functions in the entire context package: context.Background() and context.TODO(). At first glance, they look identical. They both return an empty, non-cancellable context.Context. If you check the source code (and you should, it’s brilliantly simple), you’ll see they literally do the same thing. So why do two things exist that do the same thing? This isn’t a design flaw; it’s a semantic signpost for you, the programmer.

I use context.Background() for one specific, blessed purpose: as the top-level, root context in my application. This is the immutable, all-encompassing void from which all other contexts are derived. Think of it as the “big bang” context. You use it in main() functions, in init() functions, in tests, and as the ultimate starting point when you’re the first link in the chain and there’s no incoming context from a request or an event.

package main

import (
    "context"
    "time"
)

func main() {
    // This is the one and only acceptable place for context.Background().
    ctx := context.Background()

    // Now, let's derive a new context with a deadline from this root one.
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel() // Always, always, ALWAYS call the cancel function.

    // Pass this deadline-aware context down to your function.
    if err := myDatabaseQuery(ctx); err != nil {
        panic(err) // Because panicking is a totally valid error handling strategy. (It's not.)
    }
}

func myDatabaseQuery(ctx context.Context) error {
    // ... do something that respects the ctx's deadline ...
    return nil
}

The Purpose of context.Background()

Its purpose is clarity and intent. When I see context.Background() in a codebase, it tells me a clear story: “This is the top of the food chain. This function or method is the initial source of a context chain.” It’s a best practice that makes your code instantly more readable. You should never be passing context.Background() down into a function that’s already several layers deep in a call stack that started with an HTTP request. If you’re deep in a request handler, you should be using the context.Context that came with the request, because it carries the cancellation signal for when the user closes their browser tab. Using Background() there would be like putting on noise-cancelling headphones so you can’t hear the fire alarm—deeply unwise.

So What’s context.TODO() For?

This is where the wit of the Go team shines. context.TODO() exists for those moments when you’re forced to add a context.Context parameter to a function to satisfy an interface or to start refactoring, but you genuinely don’t know which context to use yet. It’s a placeholder. It’s your way of saying, “Look, I know I need a context here, but the architectural plumbing isn’t ready yet. This compiles, but it’s a TODO item for future me, and future me is a brilliant and handsome individual who will figure it out.”

func myOldFunction(someArg string) error {
    // ... old code that hasn't been refactored for context ...
}

// I need to make this context-aware to implement a new interface.
// But I haven't threaded the context through the entire call stack yet.
func myNewFunction(ctx context.Context, someArg string) error {
    // For now, I'll use TODO() as a stand-in. This is a temporary state.
    result, err := somePackageFunction(context.TODO(), someArg)
    if err != nil {
        return err
    }
    // ... do something with result ...
    return nil
}

The Critical Difference: Intent

The technical output is identical. An empty context from Background() behaves exactly like one from TODO(). The difference is entirely in the intent you communicate to other developers (and your future self).

  • context.Background(): “I am deliberately starting a new, independent context chain here.”
  • context.TODO(): “I am being forced to use a context here, but the right one isn’t available yet. This is a temporary hack and someone should fix this before it becomes a production issue.”

Static analysis tools and linters can actually look for context.TODO() and flag it in code reviews, which is a fantastic way to prevent placeholder code from accidentally slipping into production and creating those un-cancellable, resource-leaking monsters that keep SREs up at night.

The #1 Pitfall: Mindless Propagation

The biggest mistake I see isn’t choosing the wrong one; it’s blindly using Background() where you shouldn’t. If a function gives you a context (like http.Request.Context()), that is your golden ticket. It’s already wired up to all the important things like deadlines and cancellation signals. Throwing it away and using Background() instead is a cardinal sin. You’re breaking the chain of responsibility. The function that gave you that context is probably relying on you respecting it. Don’t be the person who ignores the deadline and lets a database query run for hours because a user got bored and left.

So, to recap: use Background at the top, use TODO as a glaring, obvious placeholder, and never, ever use either of them as a lazy way to avoid plumbing a proper context through your code. Your code will be more robust, and your colleagues will thank you. Or, at the very least, they’ll complain about you less.