context in database/sql & gRPC
database/sql and google.golang.org/grpc were designed around context.Context from day one.
Passing ctx into query and RPC methods ties database and network work to the same cancellation tree as your HTTP handler.
Summary
Use QueryContext, ExecContext, and BeginTx with context for every request-scoped database call.
gRPC client and server methods accept ctx as the first argument; deadlines propagate as RPC timeouts.
Streaming RPCs must check ctx.Done() between Recv and Send calls.
Recipe
Quick-reference recipe card - copy-paste ready.
func GetOrder(ctx context.Context, db *sql.DB, id string) (string, error) {
var status string
err := db.QueryRowContext(ctx, `SELECT status FROM orders WHERE id = ?`, id).Scan(&status)
return status, err
}When to reach for this:
- Replace
Query/ExecwithoutContextin any code path reachable from HTTP or gRPC handlers. - Pass handler or interceptor context into gRPC stubs unchanged except for shorter child timeouts.
- Cancel streaming loops when
ctx.Done()fires to avoid goroutine leaks.
Working Example
package main
import (
"context"
"database/sql"
"fmt"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
_ "github.com/mattn/go-sqlite3"
)
func queryStatus(ctx context.Context, db *sql.DB) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
defer cancel()
var s string
err := db.QueryRowContext(ctx, `SELECT 'shipped'`).Scan(&s)
return s, err
}
func dialGRPC(ctx context.Context) (*grpc.ClientConn, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return grpc.DialContext(ctx, "localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
}
func main() {
db, _ := sql.Open("sqlite3", ":memory:")
defer db.Close()
parent, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
status, err := queryStatus(parent, db)
fmt.Println(status, err)
_, err = dialGRPC(parent)
fmt.Println("grpc dial:", err)
}What this demonstrates:
QueryRowContexthonors parent cancel and local child timeout.grpc.DialContextblocks only until the context ends or the connection succeeds.- Child timeouts shrink within the parent budget for each subsystem.
- SQLite driver here illustrates the pattern; production pools use the same API with Postgres/MySQL drivers.
Deep Dive
How It Works
database/sqlpasses context to driverQueryerContextinterfaces; cancellation behavior depends on driver support.- Connection pool acquisition respects context - slow pool exhaustion returns when
ctxends instead of blocking forever. Txobjects useBeginTx(ctx, opts); commit and rollback are fast but in-flight statements should already use the tx context.- gRPC encodes deadlines in metadata; servers should fail fast with
codes.DeadlineExceededwhen insufficient time remains.
database/sql API Surface
| Method | Context variant |
|---|---|
| Query | QueryContext |
| Exec | ExecContext |
| Prepare | PrepareContext |
| Begin | BeginTx(ctx, *TxOptions) |
| Ping | PingContext |
gRPC Streaming Pattern
func consume(stream pb.Service_StreamServer) error {
ctx := stream.Context()
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
msg, err := stream.Recv()
if err != nil {
return err
}
_ = msg
}
}Server streams expose Context(); client streams accept the caller's ctx on the initial RPC invocation.
controller-runtime Note
Kubernetes client calls from reconcilers should pass the reconcile context into API requests so watches stop when the manager shuts down.
Gotchas
- Using Query without Context in handlers - Abandoned HTTP requests keep DB work alive. Fix: ban non-Context methods in review for request paths.
- Long transactions without deadline - Locks held after client leaves. Fix:
BeginTxwith ctx; keep transactions short. - Ignoring ctx in stream Recv loops - Server streams leak after client disconnect. Fix: select on
ctx.Done()or handleRecverrors promptly. - grpc.Dial instead of DialContext - No cancel during service discovery or TLS handshake. Fix: always
DialContextwith bounded ctx. - Child timeout longer than gRPC deadline - Server may abort before client expects. Fix: read
ctx.Deadline()server-side and clamp work. - Assuming all drivers cancel instantly - Some drivers cancel only between network round trips. Fix: combine context with statement timeouts in SQL when needed.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
db.SetConnMaxLifetime | Pool hygiene | Per-request cancel (use ctx) |
SQL statement_timeout | Driver-agnostic query cap | Replacing application ctx entirely |
| gRPC keepalive | Detect dead connections | User-facing deadline enforcement |
| ORM session without ctx | Legacy internal batch jobs | Online request handlers |
| Message queue for slow work | Minutes-long processing | Sub-second user-facing reads |
FAQs
Does every driver support cancellation?
Most modern drivers do; verify for your database in driver docs.
Pair with SQL-level timeouts as a safety net.
Should pool.Query use context?
Yes for request paths - waiting for a free connection should respect client cancel.
How do I pass metadata in gRPC?
Use metadata.NewOutgoingContext on clients and read from metadata.FromIncomingContext on servers.
Keep metadata small and wire-safe.
What about client streaming uploads?
Pass ctx to the initial Client call; check ctx.Done() between Send chunks.
Can I use the same ctx for parallel queries?
Yes - contexts are safe for concurrent reads.
Use separate transactions for writes that must isolate.
How does errgroup fit?
errgroup.WithContext cancels siblings when one query fails.
Pass the group ctx into each QueryContext.
Should migrations use context?
Long-running migrations may use context.Background() with ops-controlled cancel.
Online request handlers should not run migrations.
What status code for canceled SQL?
Map context.Canceled to client abort; DeadlineExceeded to timeout responses.
Do not treat them as generic 500 when the client initiated stop.
Does prepared statement cache ignore ctx?
PrepareContext respects ctx; cached stmts still execute with QueryContext per call.
How do interceptors help?
Unary and stream interceptors can enforce deadlines and attach values before handlers run.
Share logic between HTTP and gRPC middleware where possible.
Related
- Cancellation Propagation in HTTP Handlers - where DB ctx starts
- Deadlines & Timeouts Across Service Boundaries - per-hop budgets
- Testing Code That Accepts context.Context - fakes and timeouts
- context Values: When and When Not - gRPC metadata vs values
- context Package Best Practices - SQL and RPC conventions
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).