sync.Pool and Zero-Allocation Patterns

Updated

September 8, 2026

sync.Pool and Zero-Allocation Patterns

Overview

Reducing allocations is the highest-leverage GC optimization. sync.Pool reuses temporary objects; zero-allocation patterns avoid heap traffic entirely on hot paths.

X and production war stories hammer this constantly: pprof → mallocgc → fix allocs before spinning on GOGC.

Diagram: Pool get/put

sequence (top → bottom):
  actors: goroutine, sync.Pool
  goroutine --> sync.Pool  : Get
  sync.Pool --> goroutine  : buffer
  goroutine --> goroutine  : use reslice
  goroutine --> sync.Pool  : Put
  note: may drop on GC

sync.Pool

var bufPool = sync.Pool{
    New: func() any {
        b := make([]byte, 0, 32*1024)
        return &b
    },
}

func handle() {
    bp := bufPool.Get().(*[]byte)
    buf := (*bp)[:0]
    defer func() {
        if cap(buf) > 64*1024 { // don't retain huge buffers
            return
        }
        *bp = buf
        bufPool.Put(bp)
    }()
    // append into buf...
}

Rules:

  1. No correctness dependency — pool may drop items on GC.
  2. Reset objects before Put (clear sensitive data).
  3. Cap retention — avoid pooling multi-MB buffers forever.
  4. Prefer pooling *[]byte or structs with slices, not giant value copies.

Zero-Alloc Techniques

Technique Idea
Stack arrays var a [256]byte then a[:n] if n bounded
sync.Pool Reuse buffers across requests
strconv.Append* Append to []byte without intermediate strings
strings.Builder Single alloc for multi-part strings
Avoid fmt in hot paths Prefer AppendInt / specialized encoders
Interface-free APIs Avoid boxing in tight loops
b := make([]byte, 0, 64)
b = strconv.AppendInt(b, 42, 10)
b = append(b, ' ')
b = append(b, "ok"...)

Measuring

go test -bench=. -benchmem
go test -bench=. -memprofile=mem.out
go tool pprof mem.out
testing.AllocsPerRun(1000, func() { /* hot path */ })

Anti-Patterns

  • Pooling when objects are already tiny and short-lived (overhead > gain)
  • Sharing pooled buffers across goroutines without ownership discipline
  • Forgetting to reslice to zero length (buf[:0])

Experiment

go test -bench=. -benchmem
package pool_test

import (
    "strconv"
    "sync"
    "testing"
)

var pool = sync.Pool{New: func() any { b := make([]byte, 0, 64); return &b }}

func BenchmarkAllocFmt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = strconv.Itoa(i) + "x"
    }
}

func BenchmarkAppendPool(b *testing.B) {
    for i := 0; i < b.N; i++ {
        bp := pool.Get().(*[]byte)
        buf := (*bp)[:0]
        buf = strconv.AppendInt(buf, int64(i), 10)
        buf = append(buf, 'x')
        *bp = buf
        pool.Put(bp)
    }
}

What to notice: Append + pool often wins on alloc/op versus string concat.

Try next: Profile a JSON encoder hot path; see if json.Encoder reuse or code generation helps more than pooling.