Frontend Labs
⋯

Day 23 — sync.Pool & Memory Efficiency

Learning Objectives

  • Reduce garbage collection pressure in hot paths using sync.Pool
  • Benchmark allocations with -benchmem
  • Understand when NOT to use sync.Pool

Key Concepts

// sync.Pool — recycles objects to reduce GC pressure
// Pool contents are garbage collected between GC cycles (not permanent!)
var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer)  // called when pool is empty
    },
}

// Get a buffer, use it, return it
func processRequest(body io.Reader) string {
    buf := bufPool.Get().(*bytes.Buffer)
    buf.Reset()             // CRITICAL: reset before use
    defer bufPool.Put(buf)  // return after use

    io.Copy(buf, body)
    return buf.String()
}

// When NOT to use Pool:
// - Objects with long lifetimes (cache entries, connections)
// - Objects that hold references to other objects (pool contents are dropped on GC)
// - Small objects (allocation cost < pool overhead)

Exercise: Buffer Pool Benchmark

// pool_bench_test.go
package pool_test

import (
    "bytes"
    "io"
    "strings"
    "sync"
    "testing"
)

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

// Without pool: allocates new buffer every call
func processWithAlloc(body string) string {
    var buf bytes.Buffer
    io.Copy(&buf, strings.NewReader(body))
    return buf.String()
}

// With pool: reuses buffers across calls
func processWithPool(body string) string {
    buf := bufPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufPool.Put(buf)
    io.Copy(buf, strings.NewReader(body))
    return buf.String()
}

func BenchmarkWithAlloc(b *testing.B) {
    body := strings.Repeat("x", 4096)
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        processWithAlloc(body)
    }
}

func BenchmarkWithPool(b *testing.B) {
    body := strings.Repeat("x", 4096)
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        processWithPool(body)
    }
}

// Run: go test -bench=. -benchmem -count=3 ./...
// Expected:
// BenchmarkWithAlloc   500000   2340 ns/op   4096 B/op   1 allocs/op
// BenchmarkWithPool    800000   1480 ns/op      0 B/op   0 allocs/op

Real-World Context

Your WAF body scanner reads and buffers every request body. At 10k rps with 4KB average body: without pool = 40MB/s of allocations = constant GC. With pool = 0 allocations in steady state. This is why high-performance proxies (Envoy, nginx) use pre-allocated buffer rings.


Generated from week4.md · 8 Oct 2026 16:36