Skip to content

Arena Allocator

An arena allocator is an allocator that frees many allocations at once.

An arena allocator is an allocator that frees many allocations at once.

This is useful when several pieces of memory have the same lifetime.

For example, imagine a parser. It may allocate many small objects while reading a file:

tokens
syntax nodes
temporary strings
metadata

You usually do not want to free each object one by one. You want to keep all of them while parsing, then free everything when parsing is done.

That is the main idea of an arena:

allocate many times
free once

A First Arena Example

An arena allocator needs a backing allocator. The backing allocator provides large blocks of memory. The arena then serves smaller allocations from those blocks.

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit();

    const allocator = arena.allocator();

    const name = try allocator.dupe(u8, "Ada");
    const city = try allocator.dupe(u8, "London");

    std.debug.print("{s} from {s}\n", .{ name, city });
}

Notice that there is no allocator.free(name) and no allocator.free(city).

The memory is released here:

defer arena.deinit();

When the arena is deinitialized, all memory allocated from it is released together.

Why Arenas Are Useful

Arenas are useful when memory has a simple lifetime.

Suppose you read one configuration file. During parsing, you allocate keys, values, arrays, and temporary structures.

After parsing, you may either keep the final result or discard everything.

If everything allocated during parsing belongs to the parsing step, an arena works well:

create arena
parse file
use parsed data
destroy arena

This is easier than tracking hundreds of individual frees.

Arena Allocation Is Fast

An arena is often fast because it does not need to manage each allocation separately.

A general purpose allocator must support complex behavior:

allocate A
allocate B
free A
allocate C
free B
free C

An arena has a simpler pattern:

allocate A
allocate B
allocate C
free all at once

Because the pattern is simpler, the allocator can be simpler.

This does not mean arenas are always faster in every program, but they are often efficient for temporary data.

The Main Tradeoff

The main tradeoff is that individual frees usually do not matter.

If you call free on memory from an arena, the arena may not return that specific piece of memory immediately. The normal way to release arena memory is to destroy or reset the whole arena.

This means arenas can use more memory if you keep allocating for a long time.

Bad use:

create one arena when the server starts
use it for every request forever
never reset it

That arena will keep growing.

Better use:

create an arena for one request
handle the request
destroy the arena

Or:

create an arena for one compiler pass
run the pass
destroy the arena

An arena should match a clear lifetime.

Request-Scoped Memory

A common use is request-scoped memory in a server.

Each request gets its own arena:

const std = @import("std");

fn handleRequest(parent_allocator: std.mem.Allocator) !void {
    var arena = std.heap.ArenaAllocator.init(parent_allocator);
    defer arena.deinit();

    const allocator = arena.allocator();

    const path = try allocator.dupe(u8, "/users/123");
    const response = try std.fmt.allocPrint(
        allocator,
        "requested path: {s}",
        .{path},
    );

    std.debug.print("{s}\n", .{response});
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    try handleRequest(gpa.allocator());
}

The request handler can allocate freely. When the request ends, arena.deinit() frees all request memory.

This keeps cleanup simple.

Arena-Owned Data Must Not Escape

The most important rule is this:

Do not use arena memory after the arena is destroyed.

Look at this broken example:

const std = @import("std");

fn makeName(parent_allocator: std.mem.Allocator) ![]u8 {
    var arena = std.heap.ArenaAllocator.init(parent_allocator);
    defer arena.deinit();

    const allocator = arena.allocator();
    const name = try allocator.dupe(u8, "Ada");

    return name;
}

This function returns name, but name was allocated from the arena. When the function returns, arena.deinit() runs. The returned slice points to memory that has already been freed.

That is a dangling slice.

The caller receives a slice, but the memory behind it is no longer valid.

A correct version should allocate returned memory from an allocator that outlives the function:

const std = @import("std");

fn makeName(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.dupe(u8, "Ada");
}

Or the arena must live outside the function:

const std = @import("std");

fn makeName(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.dupe(u8, "Ada");
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit();

    const name = try makeName(arena.allocator());

    std.debug.print("{s}\n", .{name});
}

Here, the arena lives until the end of main, so using name inside main is safe.

Using deinit

The simplest way to clean up an arena is:

defer arena.deinit();

This releases all memory owned by the arena.

A common structure is:

var arena = std.heap.ArenaAllocator.init(parent_allocator);
defer arena.deinit();

const allocator = arena.allocator();

// allocate many values

Keep this shape in mind. You will see it often in real Zig code.

Resetting an Arena

Sometimes you want to reuse the same arena object for multiple rounds of work.

For example:

parse file 1
clear arena
parse file 2
clear arena
parse file 3
clear arena

In this case, you can reset the arena instead of destroying and recreating it each time.

Conceptually, resetting means:

free the memory used by previous allocations
keep the arena ready for new allocations

The exact API can change across Zig versions, so check the standard library docs for your installed Zig version before using reset behavior in production code. The beginner habit is simpler: use deinit first, then learn reset when you need it.

Arena Allocator vs General Purpose Allocator

A general purpose allocator is flexible. It supports allocation and freeing in many different orders.

An arena allocator is specialized. It works best when many allocations die together.

AllocatorBest ForCleanup Style
General purpose allocatorMixed lifetimesFree each allocation
Arena allocatorSame lifetimeFree all at once
Fixed buffer allocatorFixed memory limitFree depends on usage
Page allocatorPage-level backing memoryLow-level cleanup

The arena allocator is not a replacement for every allocator. It is a tool for a specific lifetime pattern.

A Practical Example: Building a List of Words

Suppose we want to copy several words into memory.

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit();

    const allocator = arena.allocator();

    const words = [_][]const u8{
        "red",
        "green",
        "blue",
    };

    var copied = std.ArrayList([]u8).init(allocator);

    for (words) |word| {
        const copy = try allocator.dupe(u8, word);
        try copied.append(copy);
    }

    for (copied.items) |word| {
        std.debug.print("{s}\n", .{word});
    }
}

Here, both the ArrayList storage and the copied strings come from the arena.

We do not free each string. We do not deinitialize the list separately in this beginner example because the arena will release its backing memory all at once.

The whole group has one lifetime:

valid until arena.deinit()

When Not to Use an Arena

Do not use an arena when each object needs an independent lifetime.

For example, suppose you have a cache where entries are added and removed over time.

insert item A
insert item B
remove item A
insert item C
remove item B

This is not a good arena pattern. A general purpose allocator is better because each item has its own lifetime.

Also avoid arenas when returned values must outlive the arena. Returning arena-owned memory from a short-lived function is a common beginner mistake.

The Core Idea

An arena allocator is useful when many allocations share one lifetime.

It changes cleanup from this:

free item 1
free item 2
free item 3
free item 4

to this:

free the arena

This is simple, fast, and easy to reason about when the lifetime is clear.

The rule is:

Use an arena when the allocated data dies as a group.