Home C / C++ C++ Memory Pools — How Exhaustion Crashed a Trading Engine
Advanced 6 min · March 06, 2026
Memory Pool Allocators in C++

C++ Memory Pools — How Exhaustion Crashed a Trading Engine

Fixed-capacity pool of 100,000 slots returns null on exhaustion, causing crashes.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Written from production experience, not tutorials.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 30 min
  • Deep production experience
  • Understanding of internals and trade-offs
  • Experience debugging complex systems
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • A memory pool pre-allocates a large block and serves fixed-size slots in O(1) time
  • Heap fragmentation disappears because all allocations are same size from a contiguous region
  • Pool allocators eliminate per-allocation lock contention using thread-local caches
  • Typical allocation latency: ~10-20ns vs 100-300ns for malloc (varies by implementation)
  • Production failure: pool exhaustion causes silent crashes when objects outlive pool lifetime
  • Biggest mistake: using pool for variable-size objects, causing internal fragmentation and wasted space
✦ Definition~90s read
What is Memory Pool Allocators in C++?

A memory pool allocator (or fixed-size allocator) pre-allocates a large contiguous block of memory and divides it into slots of equal size. When you request an allocation, it returns a pointer to the next free slot — no searching, no coalescing. When you free, it marks the slot as available, often by pushing it onto a free list.

Imagine you run a parking lot.

You'll find pool allocators in game engines, trading systems, embedded firmware, and custom STL allocators. They shine when you allocate many objects of the same type (e.g., particles, network packets, database rows) and you control their lifetimes tightly.

The key difference from general-purpose allocators is that pools don't handle arbitrary sizes. That's a feature, not a bug — by giving up generality, you gain speed and predictability.

Plain-English First

Imagine you run a parking lot. Instead of letting cars park anywhere they want — creating gaps and making it hard to fit new ones — you pre-paint numbered spaces before the day starts. Every car goes into a pre-assigned slot instantly, no searching, no gaps. A memory pool allocator does exactly this for your program's RAM: it grabs a big chunk of memory upfront, slices it into fixed-size slots, and hands them out in microseconds — no rummaging through the heap, no fragmentation.

In a game engine rendering 120 frames per second, a trading system processing 10 million orders a minute, or an embedded firmware loop that must respond in under 50 microseconds, one thing will kill you faster than a logic bug: unpredictable memory allocation latency. The default new and delete operators are general-purpose tools — they handle any size, any time, with synchronization locks baked in. That generality has a cost, and in performance-critical C++ that cost is often unacceptable.

The standard heap allocator, whether it's glibc's ptmalloc, jemalloc, or TCMalloc, must maintain bookkeeping metadata, walk free-lists of variable sizes, coalesce adjacent freed blocks, and acquire locks in multithreaded contexts. Every call to new can trigger a system call, invalidate cache lines, and introduce allocations that are milliseconds apart in memory yet logically adjacent — fragmenting your address space until reallocation itself becomes a bottleneck. Memory pool allocators solve this by inverting the model: instead of asking the OS for memory on demand, you reserve a large contiguous arena upfront and serve allocations from it yourself, with full knowledge of your objects' sizes and lifetimes.

By the end of this article you'll understand how pool allocators work at the bit level, how to implement a thread-safe fixed-size pool and a more flexible slab-style allocator from scratch, how to plug them into STL containers via a custom allocator, and exactly when the tradeoffs make sense — and when they don't. You'll also leave with the mental model interviewers are probing for when they ask about custom allocators in systems programming interviews.

What is a Memory Pool Allocator?

A memory pool allocator (or fixed-size allocator) pre-allocates a large contiguous block of memory and divides it into slots of equal size. When you request an allocation, it returns a pointer to the next free slot — no searching, no coalescing. When you free, it marks the slot as available, often by pushing it onto a free list.

You'll find pool allocators in game engines, trading systems, embedded firmware, and custom STL allocators. They shine when you allocate many objects of the same type (e.g., particles, network packets, database rows) and you control their lifetimes tightly.

The key difference from general-purpose allocators is that pools don't handle arbitrary sizes. That's a feature, not a bug — by giving up generality, you gain speed and predictability.

io/thecodeforge/pool_allocator.hCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// TheCodeForge — Fixed-size pool allocator
#include <cstddef>
#include <cassert>
#include <new>

namespace io::thecodeforge {

class FixedPool {
    struct Slot {
        union {
            Slot* next;  // free list pointer
            char data[1]; // placeholder, actual size at runtime
        };
    };

    Slot* pool_start_;
    Slot* free_head_;
    size_t slot_size_;
    size_t slot_count_;

public:
    FixedPool(size_t slot_size, size_t slot_count)
        : slot_size_(slot_size), slot_count_(slot_count) {
        if (slot_size < sizeof(Slot*))
            slot_size_ = sizeof(Slot*); // ensure we can store free ptr

        pool_start_ = static_cast<Slot*>(::operator new(slot_size_ * slot_count_));
        free_head_ = pool_start_;
        for (size_t i = 0; i < slot_count_ - 1; ++i) {
            Slot* current = reinterpret_cast<Slot*>(
                reinterpret_cast<char*>(pool_start_) + i * slot_size_);
            current->next = reinterpret_cast<Slot*>(
                reinterpret_cast<char*>(pool_start_) + (i + 1) * slot_size_);
        }
        // last slot points to null
        Slot* last = reinterpret_cast<Slot*>(
            reinterpret_cast<char*>(pool_start_) + (slot_count_ - 1) * slot_size_);
        last->next = nullptr;
    }

    void* allocate() {
        if (!free_head_) throw std::bad_alloc();
        Slot* slot = free_head_;
        free_head_ = slot->next;
        return slot;
    }

    void deallocate(void* ptr) {
        Slot* slot = static_cast<Slot*>(ptr);
        slot->next = free_head_;
        free_head_ = slot;
    }

    ~FixedPool() {
        ::operator delete(pool_start_);
    }
};

} // namespace
Mental Model
Think of It Like a Checkout Counter
A pool allocator is like a stack of clean trays — you grab the top one, and when you're done you put it back on top. No hunting.
  • Pre-allocated memory = stack of trays
  • free_head_ = pointer to the next tray
  • allocate = pop the tray (O(1))
  • deallocate = push it back (O(1))
📊 Production Insight
A simple free-list pool without bounds checking is the most common source of heap corruption in game engines.
Double-free is invisible until a completely unrelated object gets corrupted.
Rule: always fill freed slots with a poison pattern and check free-list membership before every free.
🎯 Key Takeaway
Pools turn allocation into a pointer swap — O(1), deterministic, no locks per allocation.
The cost is wasted memory if slots are larger than needed (internal fragmentation).
Choose pool only when object size and lifetime are well understood.
When to Use a Fixed-Size Pool
IfAll allocations are the same size (e.g., particles, messages, db connections)
UseFixed-size pool — simple, fast, no fragmentation.
IfAllocations vary in size but only a few distinct sizes
UseSlab allocator (multiple pools, each for a size class).
IfLifetimes are nested or stack-like (LIFO)
UseArena/region allocator — even simpler than a pool.
IfYou need to free individual objects in any order
UsePool allocator with free list is appropriate.
memory-pool-allocators-cpp Memory Pool Allocator Stack Layered design from application to hardware Application Layer Trading Engine | Order Book | Market Data Handler STL Custom Allocator std::vector | std::map Pool Allocator Core Slab Allocator | Free List Manager | Thread-Local Caches Memory Management Fixed-Size Blocks | Arena Pattern | Fallback to Heap OS & Hardware Virtual Memory | Page Tables | NUMA Nodes THECODEFORGE.IO
thecodeforge.io
Memory Pool Allocators Cpp

Slab Allocator: Handling Multiple Sizes

A slab allocator maintains several fixed-size pools (slabs) for different size classes. When you request 24 bytes, it returns a chunk from the 32-byte slab. This trades external fragmentation for some internal fragmentation per slab, but still avoids the overhead of walking free lists of variable sizes.

The Linux slab allocator popularised this approach. It also caches recently freed objects in a per-CPU cache to avoid locking entirely.

Implementing a simple slab allocator means creating an array of FixedPool objects, each with a size from a predefined size table (usually powers of two plus some mid-range entries: 16, 32, 64, 128, ...). The allocator rounds up the requested size to the next slab size.

In production, slab allocators work well for networking stacks and kernel-level memory management.

io/thecodeforge/slab_allocator.hCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// TheCodeForge — Slab allocator with fixed size classes
#include <array>
#include <cstddef>
#include <new>
#include <algorithm>

namespace io::thecodeforge {

template <size_t... Sizes>
class SlabAllocator {
    static constexpr std::array<size_t, sizeof...(Sizes)> kSizes = {Sizes...};

    struct Slab {
        size_t slot_size;
        FixedPool* pool;
        Slab(size_t sz) : slot_size(sz), pool(nullptr) {}

        void ensure_pool(size_t count) {
            if (!pool)
                pool = new FixedPool(slot_size, count);
        }
    };

    Slab slabs_[sizeof...(Sizes)];  // one per size class
    size_t slab_count_ = sizeof...(Sizes);

public:
    SlabAllocator(size_t slots_per_slab = 256) {
        size_t idx = 0;
        ((slabs_[idx++] = Slab(Sizes)), ...);  // C++17 fold expression
    }

    void* allocate(size_t bytes) {
        for (auto& slab : slabs_) {
            if (bytes <= slab.slot_size) {
                slab.ensure_pool(256);
                return slab.pool->allocate();
            }
        }
        // no matching slab — fallback to malloc
        return ::operator new(bytes);
    }

    void deallocate(void* ptr, size_t bytes) {
        for (auto& slab : slabs_) {
            if (bytes <= slab.slot_size) {
                slab.pool->deallocate(ptr);
                return;
            }
        }
        ::operator delete(ptr);
    }
};

using SmallObjectAllocator = SlabAllocator<8, 16, 32, 64, 128, 256, 512, 1024>;

} // namespace
🔥Why Not Just Use Powers of Two?
Pure power-of-two slabs waste up to 50% of memory on average. Real slab allocators add mid-size entries to keep internal fragmentation under ~12%. For example, jemalloc uses size classes like 8, 16, 32, 48, 64, 80, 96, 112, 128, ...
📊 Production Insight
Slab allocators complicate deallocation because you need to know the original size to find the right slab.
If you lose that information, you either store it (metadata overhead) or fallback to a full scan.
Rule: always store the slab index or size class inside the allocation header — 4 extra bytes is worth it.
🎯 Key Takeaway
Slab allocators combine the speed of pools with flexibility for multiple sizes.
You trade some internal fragmentation for deterministic allocation and no general-purpose locking.
Know your object size distribution before choosing slab sizes — garbage in, garbage out.
Choosing Between Pool and Slab
IfOnly one object type exists in the system
UseSingle pool — no slab machinery needed.
If2-10 distinct sizes, with predictable usage patterns
UseSlab with size classes. Pick classes that match real object sizes.
IfMany different sizes, all small (< 1KB)
UseSlab allocator (often integrated into malloc replacements like jemalloc).
IfLarge objects ( > 1KB) with sporadic allocations
UseDon't slab — let the general-purpose allocator handle them. Slabs waste memory on large objects.

Thread-Safety Considerations

The simple pool above uses a single free list protected by nothing. That's safe only in a single-threaded context. In multi-threaded environments, you can't share the free_head_ pointer without protection — threads will race, corrupt the list, and crash.

Three common strategies: 1. Global mutex around allocate/deallocate — simple but kills performance under contention. 2. Thread-local pools — each thread has its own pool, no sharing. Objects must be freed on the same thread that allocated them, or you need a transfer mechanism. 3. Lock-free free list — use atomic compare-and-swap (CAS) to manipulate the head pointer. This is the fastest option for moderate contention.

Let's implement a lock‑free pool using std::atomic<Slot*>.

io/thecodeforge/lockfree_pool.hCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// TheCodeForge — Lock-free fixed-size pool
#include <atomic>
#include <cstddef>
#include <new>

namespace io::thecodeforge {

class LockFreePool {
    struct Slot {
        std::atomic<Slot*> next;
    };

    Slot* pool_start_;
    std::atomic<Slot*> free_head_;
    size_t slot_size_;
    size_t slot_count_;

public:
    LockFreePool(size_t slot_size, size_t slot_count)
        : slot_size_(slot_size), slot_count_(slot_count) {
        if (slot_size < sizeof(std::atomic<Slot*>))
            slot_size_ = sizeof(std::atomic<Slot*>);

        pool_start_ = reinterpret_cast<Slot*>(::operator new(slot_size_ * slot_count_));
        for (size_t i = 0; i < slot_count_; ++i) {
            Slot* s = reinterpret_cast<Slot*>(
                reinterpret_cast<char*>(pool_start_) + i * slot_size_);
            s->next.store(i + 1 < slot_count_ ?
                reinterpret_cast<Slot*>(reinterpret_cast<char*>(pool_start_) + (i + 1) * slot_size_) : nullptr,
                std::memory_order_relaxed);
        }
        free_head_.store(pool_start_, std::memory_order_relaxed);
    }

    void* allocate() {
        Slot* old_head = free_head_.load(std::memory_order_acquire);
        Slot* new_head;
        do {
            if (!old_head) return nullptr;  // pool exhausted
            new_head = old_head->next.load(std::memory_order_relaxed);
        } while (!free_head_.compare_exchange_weak(old_head, new_head,
                                                    std::memory_order_acq_rel,
                                                    std::memory_order_acquire));
        return old_head;
    }

    void deallocate(void* ptr) {
        Slot* slot = static_cast<Slot*>(ptr);
        Slot* old_head = free_head_.load(std::memory_order_acquire);
        do {
            slot->next.store(old_head, std::memory_order_relaxed);
        } while (!free_head_.compare_exchange_weak(old_head, slot,
                                                    std::memory_order_acq_rel,
                                                    std::memory_order_acquire));
    }

    ~LockFreePool() {
        ::operator delete(pool_start_);
    }
};

} // namespace
⚠ ABA Problem with Lock-Free Structures
The lock-free pool above has an ABA problem: if a slot is freed and then immediately allocated again, the CAS comparison sees the same pointer value but the list state has changed. In practice, for pools with small numbers of objects, ABA is rare. To fix it properly, use tagged pointers or hazard pointers.
📊 Production Insight
Thread-local pools remove all lock overhead but force per-thread object ownership.
If thread A creates an object and thread B tries to free it, you'll corrupt thread A's pool.
Rule: if cross-thread frees happen, use a global pool with a CAS free list or a hand-off queue.
🎯 Key Takeaway
Pick thread safety strategy based on access patterns, not fear of locks.
Lock-free is not magic — it adds complexity and can still hurt under high contention.
Thread-local pools are the simplest scaler: no locks, no ABA, just memory pools per thread.
Choosing Thread Safety Strategy
IfSingle-threaded or only one thread touches the pool
UseNo sync needed — fastest path.
IfMultiple threads allocate and free on the same pool, moderate contention
UseLock-free pool with CAS (the implementation above).
IfHigh contention (>8 threads hammering the same pool)
UseThread-local pools per CPU or per thread. No sharing at all.
IfObjects are frequently freed on a different thread than they were allocated
UseUse a global pool with lock-free list or a receive queue per thread.
memory-pool-allocators-cpp Pool Allocator vs Default Allocator Trade-offs in latency, fragmentation, and safety Memory Pool Allocator Default Allocator (malloc) Allocation Speed O(1) from free list O(log n) or slower under fragmentation Memory Fragmentation Zero internal fragmentation for fixed si High external fragmentation under load Thread Safety Requires explicit locking or TLS Thread-safe via arena or mutex Flexibility Fixed block sizes; limited to pre-alloca Arbitrary sizes; dynamic growth Failure Mode Pool exhaustion crashes if no fallback Graceful OOM handling possible Use Case Low-latency, predictable workloads General-purpose, variable allocation pat THECODEFORGE.IO
thecodeforge.io
Memory Pool Allocators Cpp

Plugging Into the STL: Custom Allocators

The STL containers like std::vector and std::map accept custom allocators via the template parameter. This lets you make std::vector> use your pool instead of new/delete.

A custom allocator must satisfy the Allocator concept: it needs allocate() and deallocate() methods, a rebind struct, and equality operators. The C++17 std::pmr::memory_resource provides a higher-level interface, but we'll show the classic approach.

Here's an adaptor that wraps our FixedPool into an STL allocator.

io/thecodeforge/pool_allocator_adaptor.hCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// TheCodeForge — STL-compatible pool allocator
#include <memory>
#include <cstddef>
#include "io/thecodeforge/fixed_pool.h"

namespace io::thecodeforge {

template <typename T>
class PoolAllocator {
public:
    using value_type = T;

    PoolAllocator(FixedPool& pool) noexcept : pool_(&pool) {}

    template <typename U>
    PoolAllocator(const PoolAllocator<U>& other) noexcept
        : pool_(other.pool_) {}

    T* allocate(std::size_t n) {
        if (n != 1) throw std::bad_alloc();  // pool is single-slot only
        return static_cast<T*>(pool_->allocate());
    }

    void deallocate(T* p, std::size_t n) noexcept {
        if (n == 1) pool_->deallocate(p);
    }

    template <typename U>
    struct rebind {
        using other = PoolAllocator<U>;
    };

    // equality: must share the same pool
    friend bool operator==(const PoolAllocator& a, const PoolAllocator& b) {
        return a.pool_ == b.pool_;
    }
    friend bool operator!=(const PoolAllocator& a, const PoolAllocator& b) {
        return a.pool_ != b.pool_;
    }

private:
    FixedPool* pool_;
    // allow cross-type copy
    template <typename U> friend class PoolAllocator;
};

} // namespace
💡Allocator Traps to Avoid
STL containers use rebind to allocate internal nodes (e.g., std::list nodes are different from value_type). Your allocator must support rebinding or the container will fall back to std::allocator. Also, std::vector may request n > 1 — you can't assume single-object allocation.
📊 Production Insight
STL custom allocators are swapped at compile time, not runtime.
If your pool is exhausted, the container either throws bad_alloc or if exceptions disabled, undefined behavior.
Rule: never let a container exceed its pool — use reserve() to pre-allocate.
🎯 Key Takeaway
Custom STL allocators are powerful but restrictive — they must meet the concept exactly.
If you just need fast alloc/free for a specific class, use the pool directly, not through STL.
std::pmr::memory_resource in C++17 is a cleaner abstraction for runtime polymorphic allocators.
When to Use STL Custom Allocators vs Direct Pool Usage
IfYou already use std::vector and want it to use your pool
UseWrite a PoolAllocator<T> adapter. But note: std::vector may request multiple items.
IfYou control the lifetimes explicitly (e.g., object pool pattern)
UseUse the pool directly — no need for STL layer.
IfYou need std::map or std::list on the pool
UseEnsure your allocator supports rebind — it will be used for nodes.

When Pool Allocators Lose: Trade-offs and Failure Modes

Pool allocators are not a silver bullet. They fail badly when object sizes vary widely, lifetimes are unpredictable, or memory must be returned to the OS. Here are the problems that catch teams in production:

  1. Internal fragmentation — Fixed-size slots waste space if objects are smaller than the slot. A 64-byte pool for 16-byte objects wastes 75% of memory.
  2. Memory blow-up under load — Pools never shrink. If an allocator reserves 1MB for 1024 objects of 1KB, a single peak usage event pins that 1MB forever.
  3. No way to return memory to OS — The ::operator new inside the pool is freed only when the pool destructs. Long-lived pools are effectively memory leaks from the OS perspective.
  4. Debugging difficulty — Use-after-free errors inside a pool are hard to detect because the slot memory still belongs to the pool and appears valid. Standard tools like ASan often can't catch intra-pool violations unless you poison freed slots.

For these reasons, pool allocators are best for hot paths where allocation counts are high and object sizes are uniform. For cold paths or unpredictable workloads, stick with general-purpose allocators.

io/thecodeforge/pool_failure_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// TheCodeForge — demonstrating pool pitfalls
#include <iostream>
#include <memory>
#include <vector>

struct Particle { int x, y, vx, vy; };  // 16 bytes

// Suppose pool is sized for 1000 particles at 64 bytes each
struct LargePool {
    char data[64 * 1000];
    size_t next = 0;

    void* alloc() {
        if (next >= 1000) return nullptr;
        return data + (next++ * 64);
    }
    // ... no free() - pool never releases memory
};

int main() {
    LargePool pool;
    std::vector<Particle*> vec;
    // worst-case: each particle uses only 16 bytes but pool wastes 48
    for (int i = 0; i < 1000; ++i)
        vec.push_back(static_cast<Particle*>(pool.alloc()));
    std::cout << "Wasted space: " << (64-16)*1000 << " bytes\n";
    // pool memory never freed until program exit
    return 0;
}
⚠ The Silent Memory Bloat
Pools appear to fix fragmentation, but they trade it for memory that never shrinks. In one production incident, a game's pool allocator held 2GB of 'free' memory that couldn't be given back. The fix: implement a shrink threshold that releases empty pages.
📊 Production Insight
Pool memory is sticky — once allocated from the OS, it's not released until the pool dies.
Long-lived pools accumulate pages that are never used but counted as RSS.
Rule: if your pool has a steady-state overhead > 10% of total memory, consider an arena that can decommit or an allocator that returns pages.
🎯 Key Takeaway
Pools are hot-path tools — use them where allocation count and object size are known.
Never use a pool as a general memory manager for an entire application.
The biggest production mistake: running out of pool memory and having no fallback.
When NOT to Use a Pool Allocator
IfObjects have significantly different sizes (e.g., 8 bytes vs 8KB)
UseUse slab allocator or general-purpose allocator. A single pool wastes too much.
IfPeak allocation is much higher than steady state
UsePool holds peak memory forever. Use arena or fallback to malloc for spikes.
IfAllocations and frees are rare (once every few seconds)
UseGeneral-purpose allocator is fine — pool adds complexity without benefit.
IfYou need to detect memory leaks reliably
UseCustom pools hide leaks. Use malloc replacement with built-in leak detection.

Why the Default Allocator Betrays You Under Load

You don't notice malloc fragmentation until your latency spikes 200x. The heap is a shared resource. Every thread's deallocation can dirty a cache line another thread needs. Pool allocators solve this by treating memory as a flat array of fixed-size slots. No fragmentation, no coalescing, no kernel calls. You reserve a contiguous block once, then hand out slots with a pointer bump or a free list. When every allocation is O(1) and every deallocation just pushes a pointer back onto a stack, your allocator stops being a bottleneck. The real win is CPU cache behavior: sequential allocations land on adjacent cache lines. The WHY is simple: malloc pays for generality you don't need. A pool pays for exactly your workload.

pool_allocator.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge
#include <cstddef>
#include <cstdint>
#include <new>

template<typename T, size_t PoolSize = 4096>
class PoolAllocator {
    alignas(T) char pool_[PoolSize * sizeof(T)];
    T* next_;
public:
    PoolAllocator() : next_(reinterpret_cast<T*>(pool_)) {}

    T* allocate() {
        if (next_ >= reinterpret_cast<T*>(pool_ + PoolSize * sizeof(T)))
            throw std::bad_alloc();
        return new (next_++) T();
    }

    void deallocate(T*) noexcept {
        // Fixed pool: no per-slot free. Reset pool_ pointer to start for bulk reuse.
    }

    void reset() { next_ = reinterpret_cast<T*>(pool_); }
};
Output
PoolAllocator<int, 1024> pool;
int* a = pool.allocate(); // O(1), no heap call
pool.reset(); // Reuses entire block
⚠ Production Trap:
Never use this linear allocator for objects with non-trivial destructors unless you batch-destroy. The reset() leaks destructors. Use a stack-based free list for per-object lifetimes.
🎯 Key Takeaway
Pool allocators win on latency predictability and cache locality. Introduce them when your profiler shows malloc dominating your p99 tail.

Stack Allocator: The Arena Pattern for Transient Work

Your game loop or request handler allocates thousands of small objects, then frees them all at once. Default allocators thrash the heap for no reason. The arena (or stack allocator) is your weapon: grab a large contiguous buffer, bump a pointer for each allocation, and reset the pointer when the frame or request ends. No deallocation calls during the hot path. No fragmentation. No thread contention if each thread owns its arena. The WHY: your allocation pattern is LIFO or bulk-free. You don't need individual frees. This is how game engines handle per-frame allocations and how HTTP servers handle per-request buffers. The cost is one: you cannot free a single object—only the whole arena. That constraint forces you to write code that batches lifetimes.

arena.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// io.thecodeforge
#include <cstddef>
#include <cstdint>

class Arena {
    char* const start_;
    char* current_;
    const size_t size_;
public:
    Arena(size_t bytes) 
        : start_(static_cast<char*>(::operator new(bytes)))
        , current_(start_), size_(bytes) {}

    ~Arena() { ::operator delete(start_); }

    void* alloc(size_t sz, size_t align) {
        // Align pointer up, then check capacity
        uintptr_t raw = reinterpret_cast<uintptr_t>(current_);
        uintptr_t aligned = (raw + align - 1) & ~(align - 1);
        if (aligned + sz > reinterpret_cast<uintptr_t>(start_) + size_)
            return nullptr;
        current_ = reinterpret_cast<char*>(aligned + sz);
        return reinterpret_cast<void*>(aligned);
    }

    void reset() { current_ = start_; }
};
Output
Arena frame(1024 * 1024); // 1 MB per frame
Entity* e = new (frame.alloc(sizeof(Entity), alignof(Entity))) Entity();
frame.reset(); // All memory reclaimed atomically
🔥Pattern Match:
Pair arena allocation with placement new for objects that need constructors. Call destructors manually if necessary before reset(), or skip if POD.
🎯 Key Takeaway
Arenas eliminate per-object deallocation costs. Use them when lifetimes align with scopes or frames. The reset is your free; make it explicit.

std::pmr::memory_resource: Polymorphic Allocators

Polymorphic allocators, introduced in C++17 via std::pmr::memory_resource, decouple container allocation from the underlying memory source. Instead of hardcoding new/delete or a custom allocator type, containers like std::pmr::vector accept a memory_resource* at construction, allowing runtime selection of allocation strategies. This is especially useful in trading engines where different subsystems (e.g., order book, market data) may require distinct pool behaviors.

A memory_resource is an abstract base class with two pure virtual functions: do_allocate(size_t bytes, size_t alignment) and do_deallocate(void* p, size_t bytes, size_t alignment). The standard provides concrete resources: std::pmr::new_delete_resource() (default heap), std::pmr::null_memory_resource() (throws std::bad_alloc on any allocation), and std::pmr::monotonic_buffer_resource (a fast arena allocator).

To use a pool, you can wrap it in a custom memory_resource or use the standard pool resources. For example, a monotonic buffer resource can be used for transient work: ```cpp #include #include #include

int main() { std::array buffer; std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size()); std::pmr::vector vec(&pool); vec.push_back(42); // allocates from buffer // No deallocation needed; buffer is released when pool is destroyed } `` This pattern eliminates per-element free` overhead, ideal for high-frequency trading loops where allocation bursts are followed by bulk teardown.

pmr_example.cppCPP
1
2
3
4
5
6
7
8
9
10
#include <pmr/memory_resource>
#include <pmr/vector>
#include <array>

int main() {
    std::array<std::byte, 1024> buffer;
    std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size());
    std::pmr::vector<int> vec(&pool);
    vec.push_back(42);
}
🔥Runtime Polymorphism Without Virtual Overhead
📊 Production Insight
In a trading engine, use monotonic_buffer_resource for per-message processing and synchronized_pool_resource for shared structures like order books to balance speed and thread safety.
🎯 Key Takeaway
Polymorphic allocators let you swap memory strategies at runtime, enabling pool usage across containers without template bloat.

C++23: std::pmr::synchronized_pool_resource and unsynchronized_pool_resource

C++23 introduces two new standard pool resources: std::pmr::synchronized_pool_resource and std::pmr::unsynchronized_pool_resource. These are thread-safe and non-thread-safe versions, respectively, of a pool allocator that manages fixed-size blocks. They are designed to reduce fragmentation and improve cache locality for frequent allocations of similar sizes.

Both resources maintain pools of memory chunks, each chunk subdivided into blocks of a specific size. When a request arrives, the resource picks the pool whose block size is large enough (rounded up to the nearest power of two). This minimizes waste and speeds up allocation/deallocation. The synchronized variant uses mutexes internally, making it safe for concurrent access; the unsynchronized variant is faster but requires external synchronization.

Example usage: ```cpp #include <pmr/memory_resource> #include <pmr/vector> #include <pmr/synchronized_pool_resource>

int main() { std::pmr::synchronized_pool_resource pool; // uses new_delete as upstream std::pmr::vector<int> vec(&pool); vec.reserve(1000); // allocates from pool // Thread-safe allocations from multiple threads } `` These resources are ideal for trading engines where multiple threads allocate and deallocate objects of varying sizes (e.g., order objects, trade records). The synchronized variant can replace custom thread-safe pools, reducing code complexity. However, they still rely on an upstream resource (default new_delete`) for large allocations, so monitor for unexpected upstream calls.

pool_resource_example.cppCPP
1
2
3
4
5
6
7
8
9
#include <pmr/memory_resource>
#include <pmr/vector>
#include <pmr/synchronized_pool_resource>

int main() {
    std::pmr::synchronized_pool_resource pool;
    std::pmr::vector<int> vec(&pool);
    vec.reserve(1000);
}
💡Pool Resource Tuning
📊 Production Insight
Use unsynchronized_pool_resource per-thread for lock-free performance, and synchronized_pool_resource for shared data structures. Profile to ensure pool sizes match your allocation patterns.
🎯 Key Takeaway
C++23's pool resources provide standard, efficient fixed-size allocators with optional thread safety, reducing the need for custom pool implementations.

Custom Memory Resource Implementation Pattern

When standard pool resources don't meet your latency or fragmentation requirements, implementing a custom memory_resource gives you full control. The pattern involves inheriting from std::pmr::memory_resource and overriding do_allocate, do_deallocate, and do_is_equal. This allows you to integrate existing pool allocators (e.g., a slab allocator) into the polymorphic allocator framework.

A minimal custom resource that wraps a simple bump allocator: ```cpp #include #include

class BumpResource : public std::pmr::memory_resource { char buffer; size_t capacity; size_t offset = 0; public: BumpResource(char buf, size_t cap) : buffer(buf), capacity(cap) {} private: void do_allocate(size_t bytes, size_t alignment) override { // Align offset size_t aligned = (offset + alignment - 1) & ~(alignment - 1); if (aligned + bytes > capacity) throw std::bad_alloc(); void ptr = buffer + aligned; offset = aligned + bytes; return ptr; } void do_deallocate(void, size_t, size_t) override { / no-op */ } bool do_is_equal(const memory_resource& other) const noexcept override { return this == &other; } }; `` This bump allocator is extremely fast (no free) and perfect for transient work. For a full-featured pool, you'd manage free lists and reuse blocks. The do_is_equal` method is critical for container operations like swap; it should return true only if both resources are identical (same pool).

Custom resources allow you to implement advanced strategies: thread-local caches, NUMA-aware allocation, or leak detection. In a trading engine, you might create a resource that allocates from a pre-allocated shared memory segment for inter-process communication.

custom_resource.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <pmr/memory_resource>
#include <cstddef>

class BumpResource : public std::pmr::memory_resource {
    char* buffer;
    size_t capacity;
    size_t offset = 0;
public:
    BumpResource(char* buf, size_t cap) : buffer(buf), capacity(cap) {}
private:
    void* do_allocate(size_t bytes, size_t alignment) override {
        size_t aligned = (offset + alignment - 1) & ~(alignment - 1);
        if (aligned + bytes > capacity) throw std::bad_alloc();
        void* ptr = buffer + aligned;
        offset = aligned + bytes;
        return ptr;
    }
    void do_deallocate(void*, size_t, size_t) override {}
    bool do_is_equal(const memory_resource& other) const noexcept override {
        return this == &other;
    }
};
⚠ Do Not Forget do_is_equal
📊 Production Insight
For a trading engine, implement a custom resource that uses a slab allocator for fixed-size objects (e.g., orders) and falls back to a pool for variable sizes. This minimizes fragmentation while maintaining low latency.
🎯 Key Takeaway
Custom memory_resource lets you encapsulate any allocation strategy into the polymorphic allocator framework, enabling seamless integration with STL containers.
● Production incidentPOST-MORTEMseverity: high

The Trading Engine That Crashed at Market Open

Symptom
Random segmentation faults and corrupted order fields appeared only during peak hours. The pool allocator returned seemingly valid pointers that pointed to already-freed memory.
Assumption
The team assumed the pool size was generous enough because it handled 99th percentile traffic. They never tested burst patterns.
Root cause
The pool had a fixed capacity of 100,000 slots. When that was exhausted, the custom pool returned a null pointer which the code didn't check. The null was later dereferenced, and the crash propagated. Worse, the pool's free list was corrupted by double-free from a poorly refactored object lifecycle.
Fix
Added a fallback to malloc when the pool is full, with a warning log. Also added a free-list validation (xor-linked list or magic numbers) to catch double-frees immediately. The pool size was adjusted to handle 3x the 99.9th percentile burst.
Key lesson
  • Never assume your pool will be large enough — always have a fallback allocator.
  • Check every allocation return — pools aren't guaranteed to succeed.
  • Instrument pool usage: log depletion warnings and monitor free-list integrity.
Production debug guideSymptom → Immediate Action — Real Commands and Checks5 entries
Symptom · 01
Random crashes with addresses inside pool region but objects appear corrupted
Fix
Check for double-free: enable free-list poisoning (fill freed slots with a known pattern like 0xDEADBEEF). Run with AddressSanitizer (ASan) to catch double-free and use-after-free.
Symptom · 02
Allocations start returning the same pointer repeatedly, then crash
Fix
Pool exhaustion. Add a counter to detect when the pool empty. Use fallback allocator. Check if objects are properly returned: log every allocate/free pair.
Symptom · 03
Performance drops suddenly, CPU cache misses spike
Fix
Your pool may be thrashing the cache because slots are too far apart. Verify the pool is aligned to cache line size. Use perf stat -e cache-misses to confirm.
Symptom · 04
Thread-safe pool deadlocks or livelocks under high contention
Fix
Check locking granularity. Use a per-thread cache (thread-local) instead of a single global lock. If you must share, try a lock-free stack with atomic CAS.
Symptom · 05
Memory usage grows indefinitely even though objects are freed
Fix
The pool never returns memory to the OS. If objects are freed but the pool isn't reused (e.g., arena growing unbounded), implement a size threshold to release empty chunks.
★ Pool Allocator Quick Debug Cheat SheetCommands and immediate actions for the most common pool allocator failures in production.
Double-free or use-after-free
Immediate action
Recompile with -fsanitize=address and reproduce the crash.
Commands
g++ -fsanitize=address -g myapp.cpp -o myapp && ./myapp
valgrind --tool=memcheck --leak-check=full ./myapp
Fix now
Add a free-list check: when freeing, walk the list to ensure the block isn't already there, or use a XOR linked list with verification.
Pool exhaustion (null pointer returned)+
Immediate action
Add an assertion on every allocate return and log the pool utilization percentage.
Commands
grep -rn "pool.allocate" src/ | wc -l # check all call sites
tail -f /var/log/app/error.log | grep "pool_full"
Fix now
Increase pool capacity to 3x peak usage and add a fallback to std::malloc with a warning.
Thread contention on shared pool lock+
Immediate action
Profile with perf top to see if pool_lock is the hottest function.
Commands
perf record -g -F 99 -- ./myapp && perf report
valgrind --tool=callgrind ./myapp # to see lock overhead
Fix now
Switch to per-thread pools (thread_local) or use a lock-free stack (e.g., Michael-Scott queue).
Memory Pool vs General-Purpose Allocator
AspectPool AllocatorGeneral-Purpose Allocator (malloc)
Allocation latency~10-20 ns (pointer swap)~100-300 ns (free list traversal)
Memory fragmentationNone external, some internal (slot waste)External fragmentation over time
Memory return to OSOnly at pool destructionImmediate on free of large blocks
Thread safety complexityMust be explicitly built (lock-free/thread-local)Built-in (but pay lock overhead every call)
Debugging easeHarder (intra-pool corruption)Easier (tools like ASan/Valgrind)
Best workloadFixed-size, high frequency, hot pathVariable sizes, low frequency, cold path
⚙ Quick Reference
10 commands from this guide
FileCommand / CodePurpose
iothecodeforgepool_allocator.hnamespace io::thecodeforge {What is a Memory Pool Allocator?
iothecodeforgeslab_allocator.hnamespace io::thecodeforge {Slab Allocator
iothecodeforgelockfree_pool.hnamespace io::thecodeforge {Thread-Safety Considerations
iothecodeforgepool_allocator_adaptor.hnamespace io::thecodeforge {Plugging Into the STL
iothecodeforgepool_failure_example.cppstruct Particle { int x, y, vx, vy; }; // 16 bytesWhen Pool Allocators Lose
pool_allocator.cpptemplateWhy the Default Allocator Betrays You Under Load
arena.cppclass Arena {Stack Allocator
pmr_example.cppint main() {std
pool_resource_example.cppint main() {C++23
custom_resource.cppclass BumpResource : public std::pmr::memory_resource {Custom Memory Resource Implementation Pattern

Key takeaways

1
Memory pools replace general-purpose heap with O(1), deterministic allocation from a pre-reserved arena.
2
They eliminate external fragmentation but introduce internal fragmentation and sticky memory that never returns to the OS.
3
Thread safety must be designed in from the start
shared pools need mutex, lock-free, or thread-local strategy.
4
Custom STL allocators let you integrate pools into std::vector and std::map, but you must support rebind and handle n > 1.
5
Use pools only for hot paths with fixed-size objects and controlled lifetimes. For everything else, use malloc or a slab.
6
Always handle pool exhaustion with a fallback and monitor utilization
silent corruption is worse than a crash.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Describe how a fixed-size pool allocator works internally. What are the ...
Q02SENIOR
How would you make a pool allocator thread-safe? Compare mutex, lock-fre...
Q03SENIOR
Explain the ABA problem in lock-free data structures and how it affects ...
Q04SENIOR
What is the difference between a pool allocator, an arena allocator, and...
Q01 of 04SENIOR

Describe how a fixed-size pool allocator works internally. What are the trade-offs compared to malloc?

ANSWER
A fixed-size pool pre-allocates a contiguous block of memory and divides it into equal-sized slots linked as a free list. Allocation pops from the head of the list (O(1)). Deallocation pushes back (O(1)). There's no metadata per allocation, no coalescing, no lock if single-threaded. Trade-offs: allocation is deterministic and fast (~10-20ns vs 100-300ns for malloc). Memory fragmentation is eliminated because all slots are same size (no external fragmentation), but internal fragmentation increases if objects are smaller than the slot. The pool never returns memory to the OS until destructed, so peak usage becomes permanent memory overhead. Pools work best for fixed-size, high-frequency allocations with known lifetimes.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is a memory pool allocator in simple terms?
02
When should I NOT use a memory pool allocator?
03
Can I use a pool allocator with std::vector?
04
How do I debug a pool allocator corruption?
05
What is the difference between a pool allocator and an arena allocator?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Written from production experience, not tutorials.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's C++ Advanced. Mark it forged?

6 min read · try the examples if you haven't

Previous
constexpr in C++
16 / 41 · C++ Advanced
Next
Custom Allocators in C++