A Go request is only cancellable if its context reaches the database
Context propagation is easy to demonstrate and surprisingly easy to break. Follow one request from the HTTP handler to SQL and make cancellation observable.
The hidden work after a client leaves
An HTTP client can disconnect while a handler is waiting for a database query. If the query continues, it still occupies a database connection and may consume CPU, locks or I/O for a response nobody will receive. Under load, this wasted work can amplify an incident.
Go gives each incoming HTTP request a context. The request context is cancelled when the client connection closes, an HTTP/2 request is cancelled, or the handler returns. That signal is useful only when downstream operations receive it.
Carry the request context to SQL
Prefer APIs such as QueryContext, QueryRowContext and ExecContext over their context-free counterparts in request-scoped code. The context gives the database driver an opportunity to stop work when the request is no longer relevant. A local timeout can be derived when a dependency needs a tighter budget.
- func (s *Store) FindOrder(ctx context.Context, id string) (Order, error)
- ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
- defer cancel()
- err := s.db.QueryRowContext(ctx, query, id).Scan(&order.ID)
Do not replace the caller's context
A common mistake is to create context.Background() deep inside a repository method. That detaches the database call from the request and discards cancellation from upstream. Another mistake is to pass context around as a mutable field on a long-lived service object. Context is per-operation data and should normally be the first parameter of functions that need it.
A child timeout should narrow the caller's deadline, not erase it. If the caller has 300 milliseconds remaining, deriving a fresh 2-second timeout must not make the operation live for another two seconds.
Cancellation is cooperative, not magic
The context package does not forcibly terminate arbitrary Go code. Functions must observe ctx.Done() or call context-aware APIs. Database cancellation behaviour also depends on the driver and database implementation, so verify it in the actual stack rather than assuming every query stops instantly.
For CPU-bound loops, check the context periodically at sensible boundaries. For outbound HTTP calls, construct the request with http.NewRequestWithContext. For goroutines, make sure every long-lived worker has a clear cancellation and shutdown path.
Prove it with an integration test
Use a query that deliberately takes longer than the test deadline. Call the repository method with a short context timeout and assert that it returns promptly with a cancellation or deadline error. Then inspect connection-pool statistics or run a follow-up query to ensure the connection was not stranded.
Also test cancellation caused by the incoming request, not just a timeout you create yourself. The two paths exercise different parts of the request lifecycle.
The takeaway
Context propagation is an end-to-end contract. Start with the HTTP request context, pass it through service and repository boundaries, and use context-aware I/O at the edges. If one layer silently swaps in a background context, the cancellation chain is broken exactly where the system needs it most.