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
metadataYou 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 onceA 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 arenaThis 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 CAn arena has a simpler pattern:
allocate A
allocate B
allocate C
free all at onceBecause 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 itThat arena will keep growing.
Better use:
create an arena for one request
handle the request
destroy the arenaOr:
create an arena for one compiler pass
run the pass
destroy the arenaAn 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 valuesKeep 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 arenaIn 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 allocationsThe 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.
| Allocator | Best For | Cleanup Style |
|---|---|---|
| General purpose allocator | Mixed lifetimes | Free each allocation |
| Arena allocator | Same lifetime | Free all at once |
| Fixed buffer allocator | Fixed memory limit | Free depends on usage |
| Page allocator | Page-level backing memory | Low-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 BThis 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 4to this:
free the arenaThis 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.