A value placed in a Go sync.Pool is not guaranteed to remain there until a later Get. The runtime may remove pooled items automatically, so the pool acts as a reuse opportunity rather than durable storage.
That property shapes both the performance profile and the correctness boundary of sync.Pool. Code can benefit when an object survives long enough to be reused, but it must remain correct when every Get behaves as if no prior item were available.
Pool retention is deliberately weak
The public contract permits any item stored in a pool to be removed automatically at any time without notification. If the pool contains the only reference to such an item, normal garbage collection can eventually reclaim the object.
Get has a similarly weak identity contract. It selects an arbitrary available item and may treat the pool as empty. There is no promise that a value supplied by one Put will later be returned by a particular Get.
This makes sync.Pool materially different from a bounded object cache, queue, map, or ownership registry. Those structures normally encode retention as part of their semantics. A pool does not.
Garbage collection participates in pool lifetime
The runtime connects pool cleanup to garbage collection. Internally, pool storage includes per-P local state and a victim generation used around collection boundaries. During pool cleanup, the previous victim state is dropped and current local state becomes the next victim state.
This mechanism gives recently pooled objects a limited opportunity to survive a collection boundary without turning the pool into a permanent root for idle memory. Items that remain unused can fall out of pool retention as later collection cycles advance.
The exact internal representation is a runtime implementation detail and can change between Go releases. The stable application-facing rule is narrower: pooled values may disappear, and programs cannot depend on their retention.
Reuse is an optimization, not an ownership transfer
A common pool contains temporary buffers:
var buffers = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func format(dst io.Writer, value string) {
b := buffers.Get().(*bytes.Buffer)
b.Reset()
b.WriteString(value)
_, _ = dst.Write(b.Bytes())
buffers.Put(b)
}The allocation reduction comes from successful reuse. If the runtime discards the stored buffer, Get invokes New and returns a fresh one instead. Program behavior remains the same apart from allocation and related runtime costs.
That distinction is central to safe pool use. State required for correctness cannot exist only inside the pool. A pooled object must be replaceable by an equivalent newly allocated object without changing externally visible semantics.
Large retained capacity can survive successful reuse
Weak retention does not mean immediate shrinking. A pooled object that remains available can carry its full backing storage into later operations.
For example, a bytes.Buffer that briefly grows to several megabytes may be reset and returned to a pool. Reset clears the logical contents but retains the backing allocation. If that buffer is repeatedly reused, its large capacity can remain live even though subsequent payloads are small.
This creates a tradeoff between allocation avoidance and retained heap size. Pooling objects with highly variable capacity can preserve rare peak allocations longer than expected. Some systems apply a capacity threshold and decline to return unusually large objects to the pool.
The threshold is workload-specific. It changes memory retention and allocation frequency rather than fixing a correctness defect.
Pool activity can change after quiet periods
A workload may show a high reuse rate during sustained traffic and a lower reuse rate after an idle interval. Garbage collection during the quiet period can reduce the set of pooled objects available when traffic resumes.
The resulting burst can include fresh allocations even though the same process previously had enough pooled objects for the workload. This is consistent with the pool contract and does not indicate that Put failed.
Latency measurements around bursty workloads can therefore differ from steady-state measurements. A benchmark that continuously cycles objects through a pool may measure a favorable reuse pattern that does not represent a service with long idle gaps and intervening collection cycles.
New controls empty-pool behavior
The optional New function defines what Get returns when it would otherwise return nil. For pointer-heavy temporary objects, this often keeps allocation fallback adjacent to the pool definition:
var chunks = sync.Pool{
New: func() any {
p := make([]byte, 32<<10)
return &p
},
}New does not strengthen retention. It only supplies a replacement when no pooled value is selected. A program using New should still treat each returned object as independent temporary state and reinitialize any fields that must not carry data from a previous use.
That reinitialization also prevents accidental state leakage between unrelated operations. Pool reuse preserves object identity opportunistically, including any fields the previous owner failed to clear.
Put establishes a synchronization edge for the same value
Although retention is weak, sync.Pool has a defined memory-ordering property. A Put(x) synchronizes before a Get that returns that same value x. A value returned by New has a corresponding synchronization edge before the Get that receives it.
This permits safe handoff of a pooled object between goroutines when ownership is otherwise disciplined. It does not make concurrent use of the object safe. Once a goroutine places an object into the pool, it must not continue mutating that object on the assumption that no other goroutine can obtain it.
The pool protects its own concurrent operations. The pooled object’s fields still follow their own synchronization and ownership rules.
Pooling fits disposable shared temporaries
sync.Pool is strongest when many independent operations create interchangeable temporary objects and reuse can reduce allocation pressure. Formatting buffers, encoding scratch space, and similar short-lived working objects fit that model when their state is fully reset between owners.
It is a poor semantic fit for scarce resources, connection ownership, persistent caches, rate-limit state, or objects whose presence carries application meaning. Those cases require explicit lifetime and retention rules.
The defining boundary is simple: a pooled item can vanish without notice. Every performance benefit sits behind that contract, and code using the pool remains correct only when a missing item is equivalent to allocating a replacement.