skip to content

Why can't sync.Pool be used as a connection pool or to cap how many resources exist?

level: middleimportance: should knowfreq 46%

answer

  1. one word, two different jobs
  2. no cap, no wait, no close
  3. Get just makes another one
  4. the collector can drop entries silently
  5. database/sql.DB is already the pool

basics

~20 s

sync.Pool has no maximum size, never makes a caller wait, and lets the garbage collector discard its contents at any moment without running any cleanup. A resource pool has to cap the count, block when none are free, and close what it drops.

solid answer

~40 s

The word "pool" is doing two different jobs. `sync.Pool` is a free list for interchangeable scratch objects that are cheap to recreate and need no teardown; a connection pool is a bounded set of expensive, stateful resources that must be capped, waited for, health-checked and closed. `sync.Pool` provides none of that: there is no maximum size, `Get` never blocks — it just calls `New` and manufactures another one — and the runtime may drop pooled entries during garbage collection with no hook to close them. Put connections in a `sync.Pool` and you get an unbounded number of them under load plus leaked file descriptors as the collector silently discards ones that were never closed. For database connections the right answer is `database/sql.DB`, which is itself a pool with `SetMaxOpenConns`, `SetMaxIdleConns` and `SetConnMaxLifetime`.

go deeper

for a junior

Know that the two meanings of pool are different: sync.Pool recycles throwaway scratch objects, while a connection pool manages a limited number of expensive ones. Do not reach for sync.Pool to limit anything.

for a middle

Name the specific missing properties — no maximum, no blocking Get, no close-on-eviction, contents dropped by the collector — and say what to use instead for database connections and outbound HTTP.

for a senior

Apply the three-question test in review: interchangeable, cheap to recreate, no teardown. Be able to describe the failure mode of getting it wrong, which is an unbounded object count plus file descriptors leaked without a single error log.

for a principal

Own the boundary: if a service needs a bounded resource abstraction, decide whether the team adopts an existing bounded type or builds one, and keep sync.Pool out of anything that is part of a capacity contract.

## Two things called a pool A **resource pool** manages scarce, expensive, stateful things: database connections, sockets, licences. Its whole value is in the limits — a maximum count, a queue for callers when the limit is reached, an idle timeout, a maximum lifetime, and an orderly close for anything evicted. Losing one silently is a bug; exceeding the cap is a bug. A **`sync.Pool`** is a free list for identical, disposable scratch objects — buffers, encoders, temporary structs. Its value is in avoiding an allocation. Losing one is fine: you just make another. There is no cap because there is nothing to protect. Same word, opposite guarantees. The interview question is really asking whether you know which one you are holding. ## The four properties sync.Pool does not have **No maximum.** Nothing in the API expresses a limit. If a thousand goroutines call `Get` at once, `New` runs a thousand times and you have a thousand objects. As a resource cap that is not merely weak — it is absent. **Never blocks.** A resource pool's most important behaviour is making the caller wait when everything is in use, which is what turns a capacity limit into backpressure. `sync.Pool.Get` returns immediately, always, by fabricating a new object. There is no wait, no queue, no timeout, no `context.Context` parameter anywhere in the API. **No lifecycle hook.** There is `New`, and there is nothing else. No `Close`, no eviction callback, no health check, no "is this still good" predicate. Pool a connection and the day the runtime discards it, its socket is never closed — the connection leaks until the file descriptor's finalizer, if any, gets around to it. That is a resource leak with no log line attached. **Contents may vanish.** Pooled entries are cleared by the garbage collector; a victim generation means an object typically survives a little longer than the next collection, but the pool is explicitly a cache with no retention guarantee. `Get` may also simply ignore what is stored and behave as if empty. Anything whose disappearance changes behaviour cannot live there. ## What to use instead - **Database connections:** `database/sql.DB` already *is* a pool, safe for concurrent use, shaped by `SetMaxOpenConns`, `SetMaxIdleConns` and `SetConnMaxLifetime`. Open it once, pass the `*sql.DB` around, and never wrap it in anything. - **Outbound HTTP:** reuse one `http.Client`; its `http.Transport` keeps its own keep-alive connection cache with `MaxIdleConnsPerHost`. - **Limiting how many operations run at once:** that is a bounded-concurrency problem with its own dedicated mechanisms in Go, not a job for a free list. ## What sync.Pool is genuinely for The test is three questions. Is the object interchangeable — would any instance do? Is it cheap to recreate — is losing one a non-event? Does it need no teardown — no close, no flush, no unregister? Three yeses and `sync.Pool` fits: `*bytes.Buffer` scratch space in an encoder, a reusable `gzip.Writer`, a temporary struct on a hot path. One no and it is the wrong tool. One more distinction worth stating plainly, because it comes up as a follow-up: `sync.Pool` is not a cache either. A cache is supposed to hold data you want to find again — key it, look it up, control eviction. `sync.Pool` has no keys, no lookup, and eviction you do not control. It answers "give me *a* buffer", never "give me *the* value for X".

  • Can sync.Pool be used as a cache — say, to hold parsed configuration keyed by name?
    No. It has no keys and no lookup: `Get` answers "give me any stored object", never "give me the one for this key". It also has no eviction policy you control, so entries disappear at garbage collections whether you wanted them or not. A cache needs a map plus a locking or concurrency strategy of its own.
  • What actually happens to a network connection that someone put into a sync.Pool and the runtime then discards?
    It is dropped with no `Close` call, because the pool has no eviction hook. The socket stays open until the object becomes unreachable and whatever finalizer the runtime holds for the file descriptor eventually fires — unpredictable, unbounded, and invisible in logs. Under load you exhaust file descriptors with no clear cause.
  • Would giving sync.Pool a maximum size make it usable as a resource pool?
    Not by itself. A cap only becomes a limit if `Get` waits when the cap is reached — and `Get` is defined never to wait, with no context or deadline anywhere in the API to wait against. You would also still need close-on-eviction and health checking. At that point you have written a different type.

A tray of scrap paper versus a key cabinet for company cars. Nobody counts sheets of scrap; the cars are numbered, signed out, and someone waits when they are all gone.

saying these in an interview costs you the question

  • Uses sync.Pool to limit how many connections exist
  • Expects Get to block when the pool is empty
  • Thinks pooled objects get a Close call on eviction
  • Uses sync.Pool as a keyed cache
  • Wraps an already-pooling *sql.DB in a sync.Pool