Background Context Instead Of Context Parameter

ID

go.context_params_reused_instead_creating

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Efficiency

Language

Go

Tags

best-practice, efficiency

Description

Reports context.Background() called inside a function that already receives a context.Context parameter, where the parameter should have been propagated.

Only context.Background() is reported. context.TODO() is left to go.context_todo, which already flags every occurrence of it, so the two rules never report the same line. A context parameter that cannot be propagated — unnamed, or the blank identifier _ context.Context — does not trigger the rule.

func main, func init and test entry points take no context.Context parameter, so they cannot match. Neither can a goroutine or callback body, because the enclosing function is resolved to the innermost one and a function literal declares its own parameter list: a closure meant to outlive the request is therefore never reported.

Rationale

The parameter is the caller’s context. It carries three things: the deadline for the operation in flight, the cancellation signal for it, and the request-scoped values — trace id, tenant, authenticated principal — that every downstream hop expects to find.

context.Background() is a fresh root with none of them. Work started from it ignores the caller’s deadline, keeps running after the client has disconnected and the caller’s context has been cancelled, and drops the values that make a request traceable. What was meant to be a bounded piece of work becomes an unbounded one, holding a connection, a lock or a goroutine that nothing will ever cancel.

func fetchProfile(ctx context.Context, id string) (*Profile, error) {
    root := context.Background() // FLAW — ctx is already in scope; propagate it
    timeout, cancel := context.WithTimeout(root, 2*time.Second)
    defer cancel()

    return query(timeout, id)
}

Remediation

Derive from the parameter instead of from a new root. The child then inherits the caller’s deadline and cancellation, and tightens them if it needs to.

func fetchProfile(ctx context.Context, id string) (*Profile, error) {
    timeout, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    return query(timeout, id)
}

context.Background() belongs at a genuine root — main, a server’s start-up path, a background worker’s own loop — where there is no caller and therefore no context to inherit.

When work is deliberately meant to outlive the request, say so rather than starting from a new root: context.WithoutCancel(ctx) detaches the cancellation while keeping the request’s values.

© 2026 Xygeni. All rights reserved