# How do I cut GC pressure in Go with sync.Pool and escape analysis under load?

> The enemy behind the pauses is allocation rate: kill the top allocator with pprof, pool the hot path with sync.Pool, and tune GOGC last, not first.

- Asked: 2026-05-14
- Answered: 2026-05-17
- Asked by: Arda
- Tags: performans, go
- Source: https://www.muhammetsafak.com.tr/en/just-ask/go-gc-pressure-sync-pool-and-escape-analysis-under-load/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** We have a Go data ingest service that takes 50,000 small JSON packets per second, processes them, and writes to a database. Under heavy load, when GC kicks in the CPU maxes out and the "Stop-the-World" pauses spike our API response times.

How do I apply `sync.Pool` and escape analysis in this scenario to reduce object allocations in memory?


Short answer: GC pauses are a symptom; the real enemy is **allocation rate**. Cut allocations first and GC relaxes on its own.

Your spikes aren't because GC is "bad" — 50k packets per second means hundreds of thousands of short-lived objects per second, so GC runs constantly and aggressively to collect that garbage, and the Stop-the-World pauses bleed into your response times. The fix isn't to disable GC, it's to give it less work.

1. **Reuse on the hot path with `sync.Pool`.** Instead of `new`-ing short-lived buffers, structs, and decoders for every packet, take them from a pool and return them. Critical detail: **reset** the object before you `Put` it so stale data doesn't leak. This dramatically lowers allocation pressure on the hottest path.
2. **Cut allocations at the root.** Pre-size slices/maps with `make([]T, 0, n)` when you know the count; reuse `bytes.Buffer` and the JSON decoder; avoid needless `[]byte`↔`string` copies. A single copy multiplied 50k times becomes a mountain.
3. **Run escape analysis, keep it on the stack.** The `go build -gcflags=-m` output tells you which values "escape" to the heap. Returning a pointer out of a function or boxing a value into an interface causes most escapes; keep those on the stack and GC never sees the object — the cheapest allocation is the one you never make.
4. **Don't fly blind: measure before/after with `pprof`.** The `alloc_space` profile from `pprof` tells you the line allocating the most; `GODEBUG=gctrace=1` shows GC frequency and pause duration. Measure under the same load before and after the change — what you "think" you improved is often flat once you actually measure it.
5. **Mind the pooling trap.** Pooling objects that hold pointers to other heap data can backfire: the pool keeps the object alive, so the large graph it points to never gets freed and memory swells more than you expect. Keep only flat, self-contained buffers/structs in the pool.

**Bottom line:** the order is clear — **profile → kill the top allocator → pool the survivors → then tune `GOGC`/`GOMEMLIMIT`.** `GOGC` and the soft memory limit (`GOMEMLIMIT`) make GC less frequent but won't fix a path allocating 50k times a second; they're the final tuning, not the first move. The real win comes from producing no garbage on the hottest path at all.

## Related Reading

- [Concurrency in Go: goroutines and channels in practice](/en/blog/concurrency-in-go-goroutines-and-channels-in-practice/) — Blog
- [How do I do graceful shutdown on SIGTERM during autoscaling scale-in?](https://www.muhammetsafak.com.tr/en/just-ask/graceful-shutdown-on-sigterm-for-autoscaled-apis/) — Just Ask
- [Should I run several dependent operations with errgroup so the first error cancels the rest?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-run-several-dependent-operations-with-errgroup-so-the-first/) — Just Ask
- [Should I use chunkById instead of chunk when the same job also updates the rows it iterates?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-chunkbyid-instead-of-chunk-when-the-same-job/) — Just Ask
