database/sql Connection Pooling & Timeouts
*sql.DB is a pool of connections, not a single long-lived socket.
Search across all documentation pages
*sql.DB is a pool of connections, not a single long-lived socket.
Tuning SetMaxOpenConns, idle settings, and lifetimes keeps your service from starving the database or wedging goroutines waiting for a free connection.
Pair pool limits with context.Context deadlines so abandoned HTTP requests release pool slots promptly.
SetMaxOpenConns caps simultaneous connections per process.
SetMaxIdleConns controls how many connections stay warm between bursts.
SetConnMaxLifetime and SetConnMaxIdleTime rotate connections for load balancer and credential hygiene.
QueryContext and friends honor cancellation while waiting for a connection or executing a query.
Quick-reference recipe card - copy-paste ready.
func ConfigurePool(db *sql.DB) {
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
}
func GetOrder(ctx context.Context, db *sql.DB, id string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
defer cancel()
var status string
err := db.QueryRowContext(ctx,
`SELECT status FROM orders WHERE id = $1`, id,
).Scan(&status)
return status, err
}When to reach for this:
database/sql behind HTTP or gRPC.WaitCount rising or queries outlive client disconnect.max_connections by pod count.package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
defer db.Close()
db.SetMaxOpenConns(5)
db.SetMaxIdleConns(2)
db.SetConnMaxLifetime(time.Hour)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
var one int
err = db.QueryRowContext(ctx, `SELECT 1`).Scan(&one)
fmt.Println(one, err)
s := db.Stats()
fmt.Printf("waitCount=%d waitDuration=%s\n", s.WaitCount, s.WaitDuration)
}What this demonstrates:
QueryRowContext respects the parent deadline.db.Stats() exposes WaitCount and WaitDuration for saturation signals.sql.Open creates a pool manager; connections are created lazily up to MaxOpenConns.QueryContext calls block until a connection frees or ctx ends.MaxIdleConns; otherwise they close.ConnMaxLifetime forces replacement even if the connection is healthy, which helps behind rotating credentials or PgBouncer.| Setting | Effect | Starting point |
|---|---|---|
SetMaxOpenConns | Hard cap per process | floor(db_max / replicas) |
SetMaxIdleConns | Warm connections kept | Often MaxOpenConns / 2 |
SetConnMaxLifetime | Max age before recycle | 15-60 minutes behind LB |
SetConnMaxIdleTime | Close idle past duration | 2-10 minutes |
PingContext | Health check at startup | Required in main |
ctx, cancel := context.WithTimeout(parent, 200*time.Millisecond)
defer cancel()
_, err := db.ExecContext(ctx, `SET statement_timeout = 150`)statement_timeout protects when drivers finish the current packet slowly.*sql.DB per DSN per process is typical; do not open a new pool per request.*sql.DB from main into repositories; avoid init() pools without tests.db.Stats() fields (InUse, Idle, WaitCount) on a timer in production.MaxOpenConns - A traffic spike can open thousands of connections and hit Postgres too many connections. Fix: set explicit cap from capacity planning.MaxIdleConns equal to MaxOpenConns on tiny DBs - Idle sockets waste DB slots. Fix: idle pool smaller than open cap unless latency data says otherwise.Ping at startup - Bad credentials fail on first user request. Fix: PingContext with boot timeout in main.r.Context() remaining deadline.WaitCount metrics - Saturation looks like slow queries. Fix: alert on wait duration growth before p99 query time spikes.| Alternative | Use When | Don't Use When |
|---|---|---|
| PgBouncer / RDS Proxy | Many small services share one DB | You need session-level prepared stmt stickiness without transaction mode care |
statement_timeout only | Driver cancel is weak | Replacing request-scoped context entirely |
Single connection (MaxOpenConns=1) | SQLite file locks, tiny tools | Concurrent HTTP handlers |
| ORM-managed pool | Team already standardized on GORM | You need explicit db.Stats() in ops runbooks |
sql.DB per schema | Hard multi-tenant isolation | One service with one role user is enough |
Divide database max_connections by replica count, subtract admin and migration headroom, then cap per process.
Measure WaitCount and adjust.
It prevents new uses after the deadline; in-flight work finishes unless context cancels it.
Pair with lifetimes under load balancer idle timeouts.
Not always - idle connections consume DB slots.
Start with half of max open and tune with metrics.
If no connection is free, QueryContext waits until ctx ends and returns context.DeadlineExceeded.
That is why handler ctx matters for pool health.
No - pools are process-scoped.
Requests borrow and return connections quickly.
Set MaxOpenConns(1), run two concurrent queries, assert the second respects a short ctx timeout.
Use httptest plus errgroup in integration tests.
File databases serialize writes; still set reasonable limits for in-memory test parallelism.
Networked SQL databases benefit most from tuning.
OpenConnections, InUse, Idle, WaitCount, WaitDuration, and query latency histograms tagged by statement name.
Yes - inject the same *sql.DB into both servers in main.
Separate pools only for different DSNs or isolation requirements.
Handler r.Context() should be the parent of every QueryContext.
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).
Reviewed by Chris St. John·Last updated Jul 19, 2026