Object Pooling and Memory Reuse: Reducing Allocation Overhead
You already use an object pool if your app talks to a database — the connection pool. The same idea applies to any object that's expensive to create and safe to reuse once it's finished with its previous job.
Published July 6, 2026Allocating an object usually isn't free, and in most managed languages, deallocating it isn't free either — it just happens later and less visibly, when the garbage collector reclaims it. For small, short-lived objects this overhead is negligible and not worth thinking about. It stops being negligible when an object is expensive to construct (opening a network connection, allocating a large buffer, initializing a thread) and gets created and discarded at high frequency. Object pooling addresses that specific situation: instead of creating a new instance every time one is needed and letting it get collected when it's done, you keep a pool of pre-created instances, hand one out on request, and return it to the pool instead of discarding it when the caller is finished.
The pattern itself
class ObjectPool {
constructor(factory, resetFn, size = 20) {
this.factory = factory;
this.resetFn = resetFn;
this.pool = Array.from({ length: size }, factory);
}
acquire() {
return this.pool.pop() || this.factory();
}
release(obj) {
this.resetFn(obj);
this.pool.push(obj);
}
}
The two operations that matter are acquire, which hands out an existing instance if one is free or creates a new one if the pool is exhausted, and release, which resets the object's state and puts it back for reuse. That reset step is where pooling bugs live: an object returned to the pool with leftover state from its previous use — a stale buffer that wasn't cleared, a flag that wasn't reset, a reference to data from the last caller — silently corrupts whatever the next caller does with it. This class of bug is unusually hard to reproduce, because it only shows up when a pooled object happens to get reused in a particular order, which makes it look like a race condition or a heisenbug rather than what it actually is: a reset function that missed a field.
The most common real example: connection pools
A database connection is the textbook case for pooling because setting one up is genuinely expensive — a TCP handshake, TLS negotiation if encrypted, and the database's own authentication handshake, easily tens of milliseconds combined. Opening a fresh connection for every query in a web app handling hundreds of requests per second would spend more time on handshakes than on actual queries. A connection pool keeps a set of already-authenticated connections open and hands them out to whichever request needs one, returning the connection to the pool (not closing it) when the query finishes. This is such a standard requirement that essentially every production database driver includes pooling built in or as a one-line wrapper, and it's worth reading your driver's pool-sizing documentation directly rather than guessing, since an undersized pool causes queuing under load and an oversized one can exhaust the database server's own connection limit.
Buffer pools and reducing GC pressure
The other common case is byte buffers in high-throughput systems — parsing network protocols, serializing large payloads, image or video processing. Allocating a new buffer per request in a language with garbage collection means the collector has to track and eventually reclaim every one of those short-lived allocations, and under sustained high throughput that adds up to real, measurable GC pause time. .NET's ArrayPool<T> exists specifically for this: instead of allocating a fresh array for every operation, code rents an array from the shared pool, uses it, and returns it, keeping the total number of live allocations roughly constant regardless of request volume. The effect isn't that allocation gets faster on any single call — it's that the collector has far less garbage to walk through, which shows up as fewer and shorter GC pauses under load.
When it isn't worth it
Pooling adds real complexity: a fixed-size pool can be exhausted under a traffic spike, forcing callers to wait or fall back to unpooled allocation; a poorly sized pool wastes memory holding objects nobody's using; and the reset-state bug described above is a genuine, recurring failure mode, not a hypothetical one. For objects that are cheap to construct — small structs, simple data objects, anything a modern generational garbage collector handles efficiently in its youngest generation — pooling usually makes performance worse, not better, because you've added synchronization and bookkeeping overhead to save an allocation that was already nearly free. The rule of thumb worth defaulting to: measure first, and only reach for pooling once profiling shows allocation or GC pressure from a specific expensive-to-create object actually shows up as a bottleneck, not because pooling sounds like it should help.