Go

What are the tradeoffs of sync.Pool? When does it help, and when does it hurt?

Question 391HardGo 1.22 to 1.25

sync.Pool is a per-P cache of reusable temporary objects that reduces allocation rate and GC pressure for hot, short-lived objects (buffers, encoders). Semantics you must know:

  • Items may be dropped at any time; the pool is cleared across GCs (since Go 1.13 via a victim cache, so objects survive one GC cycle). Never use it as a cache or connection pool.
  • Get may return a dirty object — always reset it.
  • Store pointers: putting a []byte directly boxes the slice header into an interface, allocating on every Put (staticcheck SA6002).
  • Don't return huge objects: one 10 MB request buffer returned to the pool pins that memory for everyone (see issue golang/go#23199). Cap the size you put back.
var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}

const maxPooled = 64 << 10

func render(w io.Writer, v any) error {
    buf := bufPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer func() {
        if buf.Cap() <= maxPooled {
            bufPool.Put(buf)
        }
    }()
    if err := json.NewEncoder(buf).Encode(v); err != nil {
        return err
    }
    _, err := w.Write(buf.Bytes())
    return err
}

It hurts when objects are cheap to allocate (small structs — the escape analysis/stack is faster), when allocation isn't in the profile, or when the lifecycle is unclear and an object is used after Put (a use-after-free style data race). Interviewer wants: "I'd add a pool only after alloc_space profiling shows the allocation site is hot, and I'd benchmark allocs/op before and after."

More on Performance, Profiling & Testing

All 38 Performance, Profiling & Testing questions