skip to content

Pool Sizing and Exhaustion

SetMaxOpenConns turns the pool into a queue: past the limit goroutines block instead of failing, and an idle limit below the open limit makes it churn connections all day.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

What does db.SetMaxOpenConns(25) do to a *sql.DB, and what happens when all 25 connections are busy?

level: juniorimportance: must knowfreq 60%

answer

  1. a ceiling, not a target
  2. in-use plus idle, one number
  3. the default is no limit at all
  4. the 26th caller waits, it does not fail
  5. the wait ends when the context does

basics

~20 s

SetMaxOpenConns caps the total connections a *sql.DB may have open at once, in-use and idle together. Once the cap is reached the next query blocks until a connection is returned rather than opening another. The default is unlimited.

solid answer

~40 s

`db.SetMaxOpenConns(25)` tells the `database/sql` pool it may never hold more than 25 connections to the database at one time, counting in-use and idle connections together. When all 25 are checked out, the next `QueryContext`, `ExecContext` or `BeginTx` call does not fail and does not open a 26th connection — it parks in the pool's wait queue until a connection comes back, or until its context is cancelled or its deadline fires, in which case the caller gets the context's error without the statement ever reaching the database. The default is 0, meaning unlimited, so an unconfigured pool under a burst will open as many connections as the server accepts. `db.Stats().WaitCount` tells you how often callers had to wait for a connection.

code

go · 3 lines
go
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(30 * time.Minute)

go deeper

for a junior

Be ready to say that a *sql.DB is a pool shared by all goroutines, that SetMaxOpenConns is a hard ceiling on in-use plus idle connections, and that leaving it unset means no limit at all.

for a middle

Explain the wait queue: a caller past the cap blocks instead of failing, and only errors when its own context is cancelled or its deadline fires, having sent nothing to the database.

for a senior

Show why capping is a choice to hold the queue inside your process, where db.Stats and per-call deadlines make it measurable and bounded, rather than letting the database refuse connections mid-request.

for a principal

Own the number as a shared resource: your cap times your maximum replica count is what the database actually sees, and that total is what you negotiate with whoever owns the server.

## A *sql.DB is a pool, not a connection A `*sql.DB` obtained from `database/sql` represents a pool of connections plus the bookkeeping around them, and it is safe for concurrent use by many goroutines. When a goroutine calls `db.QueryContext`, `db.ExecContext` or `db.BeginTx`, the pool lends it a connection for the duration of that operation and takes the connection back afterwards. No goroutine holds a connection implicitly; ownership is only extended by explicitly asking for it, for example with `db.Conn` or by beginning a transaction. ## What the cap actually counts `db.SetMaxOpenConns(n)` sets a hard ceiling on how many connections that pool may have open simultaneously. The number counts **in-use plus idle**: an idle connection is still an open socket and still an authenticated session consuming memory on the database server, so it counts. A pool configured with `SetMaxOpenConns(25)` therefore does not keep 25 busy connections plus a few spares — 25 is the whole allowance. Passing a value less than or equal to zero means *no limit*, and no limit is the default. An unconfigured `*sql.DB` will open a fresh connection for every concurrent caller that finds no idle one waiting, which under a burst is how a service discovers the database server's own connection limit the hard way: connection attempts start being refused by the server, and the failure surfaces in the middle of user requests rather than as a bounded wait you control. ## What happens when the cap is reached The pool maintains a queue of pending connection requests. A caller arriving when every connection is in use is added to that queue and blocks. Three things can end the wait: 1. Another goroutine finishes and returns a connection, which is handed straight to a waiter. 2. The caller's `context.Context` is cancelled or its deadline elapses, in which case the call returns the context's error — for a deadline, `context.DeadlineExceeded`. Nothing was sent to the database, so the server has no record of the query at all. 3. The pool is closed. What does **not** happen is equally important: the pool does not open an extra connection "just this once", it does not return an error immediately because the pool is full, and it does not multiplex two concurrent queries onto one connection. ## Why cap it at all Capping deliberately moves the queue from the database server into your own process. Inside your process the wait is visible (`db.Stats`), bounded (every call carries a context with a deadline), and cheap — a blocked goroutine costs a few kilobytes. On the server each extra connection costs memory and scheduling, and the server's limit is shared with every other client, so overrunning it is somebody else's outage as well as yours. The cap also interacts with the idle limit: if you set `SetMaxOpenConns` to a value below the current `SetMaxIdleConns` setting, the idle limit is reduced to match, because the pool cannot keep more idle connections than it is allowed to have open in total. ## Seeing the cap in effect `db.Stats()` returns a `sql.DBStats` whose fields describe exactly this: `MaxOpenConnections` (the cap you set), `OpenConnections`, `InUse`, `Idle`, and the two that reveal saturation, `WaitCount` (how many callers have had to wait) and `WaitDuration` (the total time spent waiting). `InUse` sitting at the cap with `Idle` at zero and `WaitCount` climbing is the signature of a pool that is the bottleneck. ## The cap is per process One last thing juniors are frequently caught by: the cap applies to that one `*sql.DB` value in that one process. Ten replicas of the service each configured with `SetMaxOpenConns(25)` can present 250 connections to the same database. The number the database cares about is your cap multiplied by however many processes are running, which is why the value is usually treated as a shared budget rather than a local tuning knob.

  • If SetMaxOpenConns is never called, what tends to break first under a traffic burst?
    The database server, not your process. With no cap the pool opens a connection per concurrent caller, and once the server hits its own connection limit it starts refusing new connections. Those refusals surface as errors mid-request, hit every client of that server rather than only you, and are far harder to reason about than a bounded wait inside your own pool.
  • Does the cap limit the whole service or one process?
    One `*sql.DB` in one process. Every replica has its own pool with its own cap, so the database sees your cap multiplied by the number of running processes, plus any migration jobs or cron workers that open their own pools. Peak demand should therefore be computed from your maximum replica count, not the current one.
  • How would you tell whether callers are actually waiting for a connection?
    Sample `db.Stats()` on a timer and export the fields. `WaitCount` and `WaitDuration` are cumulative totals, so diff two samples: a rising `WaitCount` means callers are queueing, and the difference in `WaitDuration` divided by the difference in `WaitCount` gives the mean wait per acquire. `InUse` pinned at `MaxOpenConnections` with `Idle` at zero confirms it.

It is a car park with 25 bays and a barrier: when the bays are full the next driver queues at the barrier rather than parking on the grass, and only leaves the queue when a bay frees or they give up.

saying these in an interview costs you the question

  • Thinks the pool opens one extra connection when the cap is reached
  • Thinks a query fails immediately once the pool is full
  • Believes the default open limit is a small number rather than unlimited
  • Counts only in-use connections against the cap and treats idle ones as free
  • Assumes each goroutine keeps its own connection for its whole lifetime
open as a page

How do SetMaxIdleConns and SetMaxOpenConns differ on a *sql.DB, and what does a low idle limit cost?

level: middleimportance: must knowfreq 52%

basics

~20 s

SetMaxOpenConns caps how many connections may exist at once; SetMaxIdleConns caps how many the pool keeps when they fall idle. With the idle limit far below the open limit, connections are closed and reopened constantly under load, paying a fresh handshake each time.

open as a page

A Go worker pool's db.QueryContext calls return context deadline exceeded while the database logs no slow queries — how do you prove the connection pool is the bottleneck?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Sample db.Stats over time. Rising WaitCount with WaitDuration divided by WaitCount approaching the deadline, InUse pinned at the open limit and Idle at zero, proves callers are timing out while queueing for a connection rather than while running a query.

open as a page

How do you set SetMaxOpenConns across a service's replicas when the database's connection budget is shared with the rest of the fleet?

level: principalimportance: should knowfreq 36%

basics

~20 s

Treat the cap as a share of a granted budget, not a local tuning knob: the database sees your cap multiplied by your maximum replica count, plus jobs and migrations. Decide that total with whoever owns the database, then choose what callers experience when it is reached.

open as a page

What do SetConnMaxLifetime and SetConnMaxIdleTime cap on a *sql.DB, and why set either one?

level: middleimportance: nice to knowfreq 38%

basics

~20 s

SetConnMaxLifetime caps a connection's total age since it was created; SetConnMaxIdleTime caps how long it may sit unused. Both default to no limit. Lifetime retires connections before something outside Go kills them; idle time lets the pool shrink after a burst.

open as a page