Skip to content

Custom Allocators

A custom allocator is an allocator you design for a specific memory policy.

A custom allocator is an allocator you design for a specific memory policy.

Most beginner Zig programs use allocators from the standard library:

std.heap.GeneralPurposeAllocator
std.heap.ArenaAllocator
std.heap.FixedBufferAllocator
std.heap.page_allocator

These cover many common cases. But Zig also lets you build your own allocator when the standard choices do not match your program.

A custom allocator answers the same basic questions as every other allocator:

Where does the memory come from?
How is memory handed out?
How is memory released?
What happens when memory runs out?

The difference is that you choose the answers.

Why Write a Custom Allocator?

You usually do not write a custom allocator at the start of a project.

You write one when memory behavior itself is part of the program design.

For example, you may want an allocator that:

counts every allocation
limits total memory use
logs allocation sizes
detects suspicious allocation patterns
allocates from a special memory region
reuses fixed-size blocks very quickly
groups memory by subsystem
tracks memory by frame in a game engine

A custom allocator is useful when you need control that a normal allocator does not provide.

Allocator as an Interface

In Zig, most code accepts this type:

std.mem.Allocator

That is the allocator interface.

A function that receives std.mem.Allocator does not need to know which allocator implementation is behind it.

fn makeBuffer(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.alloc(u8, 1024);
}

The caller can pass any allocator that implements the interface:

const buffer = try makeBuffer(gpa.allocator());

or:

const buffer = try makeBuffer(arena.allocator());

or:

const buffer = try makeBuffer(my_custom_allocator);

This is the main reason custom allocators are powerful. You can change memory policy without rewriting all code that uses memory.

A Simple Wrapper Allocator Idea

The easiest custom allocator to understand is a wrapper allocator.

A wrapper allocator does not manage raw memory by itself. Instead, it wraps another allocator and adds behavior.

For example:

caller asks wrapper for memory
wrapper records information
wrapper asks parent allocator for memory
wrapper returns memory to caller

This design is easier than writing a full allocator from scratch.

A common wrapper allocator might count how many bytes were allocated.

A Counting Allocator Concept

Imagine an allocator that counts allocation requests.

The idea:

store a parent allocator
store a counter
on each allocation, increase the counter
forward the real allocation to the parent allocator

The parent allocator still does the real memory work.

The custom allocator adds measurement.

This kind of allocator is useful in tests and performance investigations.

Why Custom Allocators Are Advanced

A real allocator implementation must obey strict rules.

It must handle:

alignment
allocation size
resizing
freeing
error behavior
lifetime rules

Alignment is especially important.

Some values must live at memory addresses that are multiples of a certain number. For example, a type may need 8-byte alignment. If an allocator returns memory at a bad address, the program can become incorrect or crash.

So a custom allocator must not merely return “some bytes.” It must return memory with the right size and alignment.

That is why beginners should first learn how to use allocators before writing allocator internals.

Custom Allocators as Policy

A good way to think about custom allocators is this:

An allocator is a memory policy packaged as a value.

For example, these are different policies:

Use the operating system directly.
Use a general heap.
Use one arena and free everything together.
Use this fixed 4096-byte buffer.
Reject allocations after 1 MiB.
Log every allocation.
Allocate from GPU-visible memory.

Because Zig passes allocators as values, your program can choose the policy explicitly.

Example: Memory Limit Policy

Suppose you want part of your program to use at most 1 MiB of memory.

You could build a wrapper allocator that tracks the total amount allocated and refuses requests after the limit.

Conceptually:

limit = 1 MiB
used = 0

when allocation request arrives:
    if used + requested_size > limit:
        return OutOfMemory
    otherwise:
        allocate from parent allocator
        increase used

This can be useful when parsing untrusted input.

Without a memory limit, a malicious or broken input file might cause your program to allocate too much memory.

With a limiting allocator, the parser receives a normal std.mem.Allocator, but the allocator enforces a policy.

The parser code does not need to know about the policy:

fn parseDocument(allocator: std.mem.Allocator, input: []const u8) !Document {
    // allocate as needed
}

The caller chooses the allocator:

const document = try parseDocument(limited_allocator, input);

That separation is clean.

Example: Debug Logging Policy

Another custom allocator could log every allocation.

Conceptually:

allocate 128 bytes
allocate 4096 bytes
free 128 bytes
resize 4096 bytes to 8192 bytes
free 8192 bytes

This helps answer questions like:

Why is this function allocating so often?
Which path allocates the largest buffers?
Does this operation allocate at all?

The allocator becomes an inspection tool.

Example: Game Frame Allocator

Games often process work frame by frame.

A game might allocate temporary memory during one frame, then discard it at the end of that frame.

Conceptually:

start frame
allocate temporary physics data
allocate temporary render commands
allocate temporary UI data
end frame
clear all temporary memory

This is similar to an arena, but a game engine may want special behavior:

separate memory for each frame
statistics for debugging
warnings when frame memory grows too large
platform-specific memory layout

A custom allocator can encode that policy.

Do Not Hide Allocation

Even with custom allocators, Zig keeps allocation visible.

You should still pass the allocator into code that needs memory:

fn buildIndex(allocator: std.mem.Allocator, input: []const u8) !Index {
    // allocations are visible through the parameter
}

Avoid hiding a custom allocator in global state unless you have a specific systems-level reason.

Explicit allocator parameters make code easier to test, reuse, and audit.

Start with Composition

When designing allocator behavior, prefer composition first.

That means building from existing allocators.

For example:

page allocator
    used as backing allocator for arena
        wrapped by counting allocator
            passed to parser

Or:

general purpose allocator
    wrapped by limiting allocator
        passed to JSON loader

This style keeps each piece small.

One allocator provides raw memory. Another groups lifetimes. Another adds logging. Another enforces limits.

You do not need one large allocator that does everything.

When Not to Write a Custom Allocator

Do not write a custom allocator just because it sounds powerful.

Use a standard allocator when it already fits.

Use GeneralPurposeAllocator for ordinary heap allocation.

Use ArenaAllocator for many allocations with the same lifetime.

Use FixedBufferAllocator for a known memory buffer.

Use page_allocator for direct page-backed memory or simple backing allocation.

A custom allocator adds maintenance cost. It can also introduce subtle bugs if alignment, freeing, or resizing are wrong.

Write one when you have a specific memory rule that needs to be enforced or measured.

A Practical Beginner Rule

At the beginning, treat custom allocators as something you consume before something you implement.

That means you should learn to write functions like this:

fn loadData(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.alloc(u8, 1024);
}

rather than hard-coding a specific allocator inside the function.

This makes your function compatible with future custom allocators.

The function does not care whether the caller passes:

a general purpose allocator
an arena allocator
a fixed buffer allocator
a logging allocator
a limiting allocator

That is the point.

The Core Idea

A custom allocator lets you define a specific memory policy and pass it through normal Zig APIs.

Most code should depend only on std.mem.Allocator.

That keeps memory policy outside the function and memory use visible at the call site.

The rule is:

Write code that accepts allocators before you write allocators yourself.