A timeout says when an operation is no longer wanted. It does not prove that every piece of work behind that operation has stopped.
In Go, context.Context carries deadlines and cancellation signals. Your code has to pass that context to the work it starts, and the receiving code has to support cancellation. A timer around the outer request cannot repair a broken chain inside it.
That distinction matters when a slow database query outlives the request that started it. Here is how I would review that chain, using Go's documented behavior rather than assuming a timeout is a kill switch.
Start with the request, not a fresh background
Go's database cancellation guide shows a context derived from its caller, with a timeout added for a database operation. That parent-child relationship is the important part.
For work needed by an HTTP request, start with r.Context(). Derive a child context if that operation needs a tighter limit. If you start from context.Background() instead, you create a separate root. Canceling the request's context will not cancel that unrelated context.
A background context is not inherently wrong. It is wrong when you intend work to follow the request's lifetime but accidentally disconnect it. A durable job that must survive the request needs its own explicit lifetime and ownership. It should not survive merely because somebody forgot to pass an argument.
Pass the deadline to the operation
This function is an illustrative pattern, not a benchmark or a complete endpoint. It assumes an existing *sql.DB and an album table. It needs the standard context, database/sql and time packages.
func albumTitles(ctx context.Context, db *sql.DB) ([]string, error) { queryCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() rows, err := db.QueryContext(queryCtx, "SELECT title FROM album") if err != nil { return nil, err } defer rows.Close() var titles []string for rows.Next() { var title string if err := rows.Scan(&title); err != nil { return nil, err } titles = append(titles, title) } if err := rows.Err(); err != nil { return nil, err } return titles, nil } An HTTP handler would pass r.Context() as the first argument. The two-second value is an example, not a recommended production setting. Choose a budget that fits the endpoint and its dependencies.
Notice what the function does not do: create a context and then call db.Query without it. The timeout must reach the operation through QueryContext.
Also keep the context alive while consuming the rows. Returning rows from a helper that immediately runs a deferred cancel() would give the caller a result stream whose context has already been canceled. Here, scanning happens before the helper returns.
The database/sql documentation specifies checking rows.Err() after iteration. A loop ending is not enough evidence that all rows were read successfully.
A child timeout cannot extend its parent
The context documentation defines a derived deadline as no later than the parent's deadline. Giving a database call two seconds does not grant it two fresh seconds if the caller has less time remaining.
Think of that local timeout as an upper bound. The child can end because its own deadline expires, its parent is canceled, or its cancellation function is called. You cannot make the original request live longer by repeatedly wrapping it in new child contexts.
Keep defer cancel() even when the operation normally finishes before its deadline. Go's guide explains that calling it releases resources associated with the derived context. Waiting for the timer is unnecessary.
The driver is part of the contract
There is a limit to what the wrapper above promises. The database/sql documentation explicitly says drivers without context-cancellation support will not return until the query completes.
That means a correctly passed context is necessary, but it is not evidence that your particular database stack stopped promptly. Verify the driver and the behavior you depend on. Do not write an operational guarantee based only on the method name.
The same distinction applies to your own long-running work. The context's Done() channel tells code when cancellation is requested. It does not insert a cancellation check into an arbitrary loop. And calling a CancelFunc does not wait for the work to stop, as the context documentation makes clear.
Test the end of the chain
My review would use a controlled, non-production slow operation. Cancel its parent before the operation finishes. Separately, let the child deadline expire. Observe when the function returns and, using the database's own tooling, whether the underlying query actually ends.
Those are different observations. A handler returning an error is not by itself proof that no downstream work remains. Record cancellation and deadline expiry distinctly when that helps diagnosis, without exposing credentials or customer data.
The useful question is not "Did I set a timeout?" It is "Did cancellation reach the work, and did that work respond?" Follow that chain through the API, the result-reading code and the driver before treating the timeout as protection.
Cover photo by Dawit, free to use under the Unsplash License.
I write about startups and AI on StartupWiki.

