Go

How does sync.Pool behave, and what are its pitfalls?

Question 345HardGo 1.22 to 1.25

sync.Pool is a concurrency-safe cache of temporary, interchangeable objects. Each P has its own local shard, so Get and Put take no lock in the common case. Objects can be dropped at any time. Each GC moves the pool's contents into a victim cache, and whatever is left there is discarded at the next GC, so an idle pool empties after about two cycles.

var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}

func render(w io.Writer, name string) error {
    b := bufPool.Get().(*bytes.Buffer)
    b.Reset()
    defer func() {
        if b.Cap() <= 64<<10 { // don't retain huge buffers
            bufPool.Put(b)
        }
    }()
    b.WriteString("hello, ")
    b.WriteString(name)
    _, err := w.Write(b.Bytes())
    return err
}

Pitfalls:

  • Put pointers, not values. pool.Put(buf) with a []byte boxes the slice header into any and allocates (staticcheck SA6002). Store *[]byte or a struct pointer.
  • Always reset state. Data left over from a previous user can leak between requests, which is a security bug.
  • Do not use it for connections or anything else that needs lifecycle control. Items vanish without any close hook.
  • Unbounded size variance: a single 50 MB request can leave a huge buffer in the pool. Cap what you Put back.
  • Never use an object after you have Put it.

More on Memory, GC & Runtime Internals

All 38 Memory, GC & Runtime Internals questions