Sinclair Target

Getting My Allocators Straight

Sep 07, 2025

One of Zig’s distinguishing features is that there are no hidden memory allocations. Functions that need to allocate memory, even those in the standard library, advertise that they allocate memory by accepting an “allocator” parameter. The allocator parameter gives the caller control over which concrete allocator the function should use and therefore also control over how and where the function allocates memory.

Another of Zig’s distinguishing features is that the standard library is, let’s say, economical when it comes to documentation. There are several different concrete allocator implementations in the standard library; I’ve found it hard to understand what they all do or how they work. People not directly involved with Zig’s development have filled in some blanks by writing guides and blog posts explaining Zig’s allocators, but these resources don’t cover all the allocators I can find in the standard library (as of Zig 0.15.1) and still leave me with questions.

This is my attempt to figure out how all the available allocators work and what each one is for. What I’ve discovered here is based on the little official documentation I could find and on my (possibly incorrect) reading of standard library code.

To make things easier to wrap my head around, I’ve divided the available allocators into four categories: the primitive allocators, the basic allocators, the advanced allocators, and the utility allocators.

The Primitive Allocators

The primitive allocators directly manage memory without delegating to another allocator. More sophisticated allocators are built on top of the primitive allocators.

The primitive allocators include std.heap.FixedBufferAllocator, std.heap.PageAllocator, std.heap.SbrkAllocator, and std.heap.WasmAllocator.

The important ones here are FixedBufferAllocator and PageAllocator. SbrkAllocator seems to exist specifically to support Plan 9, which I believe does not have the mmap() system call (so sbrk() must be used instead). WasmAllocator is only relevant when targeting WebAssembly.

FixedBufferAllocator is the simplest of all the allocators. It doesn’t even really allocate anything; it must be initialized with a slice of memory. A FixedBufferAllocator will then allow you to alloc() and free() chunks from this slice. But the allocator never has access to more memory than you first initialized it with. You’d use a FixedBufferAllocator when you know at compile-time how much memory you will need and can’t just use stack memory because you require something that conforms to the std.mem.Allocator interface.

Calling free() on a FixedBufferAllocator doesn’t do anything unless the memory you are trying to free was the last thing you allocated. Typically you’d just call reset() to free all the memory at once.

PageAllocator is the primary allocator for requesting memory from the operating system. It does this, at least on POSIX platforms, using the mmap() system call. It invokes mmap() each time you call alloc() and never maps less than a page of memory, so this is not an allocator for making many small allocations. Instead, this allocator is meant to be used by other allocators to request large chunks of memory that then get managed internally.

The Basic Allocators

The “basic” allocators are the ones you’ll probably use most in Zig. They build on the primitive allocators to provide memory-handling strategies useful in most Zig programs. In my taxonomy, there are two basic allocators: std.heap.DebugAllocator and std.heap.ArenaAllocator.

DebugAllocator used to be called GeneralPurposeAllocator. It wraps PageAllocator and does its own bookkeeping so that you can performantly alloc() and free() small chunks of memory. It implements useful debug functionality like tracking leaks, printing stack traces on double-frees, etc. When you need a general-purpose allocator (i.e. one that supports lots of arbitrarily interleaved alloc() and free() calls) but are more concerned about safety than performance, DebugAllocator is the right choice.

The name DebugAllocator suggests to me that this allocator should never be used in release builds, but that doesn’t seem to be the case. The Zig compiler currently uses a DebugAllocator as its general-purpose allocator when built in “Release Safe” mode. It’s only when building in “Release Fast” or “Release Small” (or when linking with libc) that another allocator is used instead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/main.zig
const gpa, const is_debug = gpa: {
    if (build_options.debug_gpa) break :gpa .{ debug_allocator.allocator(), true };
    if (native_os == .wasi) break :gpa .{ std.heap.wasm_allocator, false };
    if (builtin.link_libc) {
        // We would prefer to use raw libc allocator here, but cannot use
        // it if it won't support the alignment we need.
        if (@alignOf(std.c.max_align_t) < @max(@alignOf(i128), std.atomic.cache_line)) {
            break :gpa .{ std.heap.c_allocator, false };
        }
        break :gpa .{ std.heap.raw_c_allocator, false };
    }
    break :gpa switch (builtin.mode) {
        .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
        .ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
    };
};

DebugAllocator is currently Zig’s “default” general-purpose allocator, but that seems likely to change in the future. SmpAllocator (discussed later) or an entirely new allocator may become the default, in which case that allocator should be considered basic and DebugAllocator should be relegated to the utility allocators.

An ArenaAllocator is sort of like a FixedBufferAllocator that can grow. Like with FixedBufferAllocator, calling free() on an ArenaAllocator doesn’t do anything unless you are freeing the last chunk of memory allocated. Instead, instances of ArenaAllocator are designed to support lots of calls to alloc() followed by a single call to reset(). You would use an ArenaAllocator to hold all the memory you need to handle a single HTTP request or to render a single frame in a video game—wherever there are independent “cycles” of computation that don’t need to share memory. Using an ArenaAllocator in cases like these can be more performant than using a general-purpose allocator.

Internally, an ArenaAllocator manages a linked list of buffers allocated using a backing allocator. This linked list grows as you request more memory with alloc(). Each new buffer that has to be added to the list is half again as large as the previous buffer. This is an optimization meant to reduce the total number of buffers that need to be allocated while keeping the memory footprint of the arena low to start.

Unlike DebugAllocator, which always uses a PageAllocator, ArenaAllocator expects you to specify the backing allocator when you initialize the struct. I find this confusing. I’m not sure why you’d want to use anything other than a PageAllocator as the backing allocator, unless perhaps you want to use a DebugAllocator to trace allocations. Using a non-debug general purpose allocator as the backing allocator might minimize memory usage if you only allocate a small amount of memory relative to the page size. That’s my best guess. But I suspect doing this would generally be less performant than using a PageAllocator.

The Advanced Allocators

These allocators are “advanced” because you might only reach for them when writing certain kinds of Zig programs. I haven’t personally used any of these allocators yet so please don’t trust what I have to say about them too much.

If you are already linking against a libc and need a general-purpose allocator, you can use the one provided by your libc. std.heap.c_allocator makes the libc allocator available. As shown in the snippet above, the Zig compiler uses the C allocator whenever it’s linked with libc.

In a multi-threaded Zig program, you shouldn’t share an allocator across threads without first ensuring it is thread-safe. std.heap.ThreadSafeAllocator is nothing more than a wrapper that mediates access to its backing allocator with a mutex. You could use it to share an ArenaAllocator across threads.

std.heap.SmpAllocator is new as of Zig 0.15.1. It is a performant, general-purpose allocator with thread-safety baked in. According to Andrew Kelley, SmpAllocator is for “Release Fast” builds that use multi-threading.

Finally, there’s std.heap.MemoryPool. I haven’t seen this used in the wild yet, but it’s a specialized allocator for allocating objects all of one type. It only supports the create() and destroy() methods, so properly speaking it isn’t an allocator (it doesn’t conform to the std.mem.Allocator interface). But I can imagine it would be useful in something like a game engine. It uses an ArenaAllocator under the hood, which it initializes with the backing allocator you pass in. So don’t make the mistake of initializing a MemoryPool with an ArenaAllocator!

The Utility Allocators

This final category includes just std.testing.allocator and std.testing.FailingAllocator. These allocators are used more for their additional behavior than for the memory management strategy they implement. As I mentioned above, DebugAllocator would belong here if Zig weren’t currently missing an alternative general-purpose allocator for single-threaded programs.

The testing allocator, std.testing.allocator, is just a DebugAllocator initialized with a configuration specific to tests. It has a safety check to make sure you don’t mistakenly use it outside of test code.

FailingAllocator is an allocator that always fails to allocate anything. (It tries hard though, the poor thing.) This is useful for making sure, in tests, that your code can handle out-of-memory errors.

Bonus: The Zig Language Reference on Allocators

The Zig Language Reference includes a helpful section on choosing an allocator. This section claims to present a “flow chart” for choosing an allocator but in fact gives you a numbered list. Here is an actual flow chart:

Zig allocator flowchart.

Choosing an allocator according to the language reference.

I should explain that “CLI tool” here refers to a command-line program that “runs from start to finish without any fundamental cyclical pattern.”