Skip to content

Page Allocator

The page allocator asks the operating system for memory directly.

The page allocator asks the operating system for memory directly.

It works at the level of memory pages. A page is a fixed-size block of memory managed by the operating system. On many systems, one page is 4096 bytes, but you should not hard-code that assumption unless you have a reason.

The page allocator is available as:

const allocator = std.heap.page_allocator;

A small example:

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    const buffer = try allocator.alloc(u8, 4096);
    defer allocator.free(buffer);

    buffer[0] = 42;

    std.debug.print("allocated {d} bytes\n", .{buffer.len});
}

This looks similar to the examples using the general purpose allocator. The difference is where the memory comes from.

With page_allocator, Zig asks the operating system for memory pages.

What Is a Page?

The operating system does not usually manage memory one byte at a time. It manages memory in blocks called pages.

A page is a unit of virtual memory.

When a program needs memory, the operating system maps pages into the process. The program can then read and write that memory.

You can think of it like this:

Your program asks for memory.
The allocator asks the operating system for pages.
The operating system maps pages into your process.
The allocator gives your program a slice.

The page allocator is close to this operating system mechanism.

Why Not Use It for Everything?

The page allocator is simple, but it is not always efficient for small allocations.

Suppose you ask for 10 bytes:

const tiny = try std.heap.page_allocator.alloc(u8, 10);
defer std.heap.page_allocator.free(tiny);

The operating system still works in pages. Internally, this may involve much more memory and overhead than 10 bytes.

For one tiny allocation, that may not matter. For thousands of tiny allocations, it matters.

This is why the page allocator is often used as a backing allocator for another allocator.

For example, a general purpose allocator may request larger chunks from the system and then manage smaller allocations itself.

Page Allocator as a Backing Allocator

You have already seen this pattern:

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();

const allocator = gpa.allocator();

The general purpose allocator manages many allocations for your program. Under the hood, it may use lower-level memory from the system.

Another common pattern is:

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

const allocator = arena.allocator();

Here, the arena uses page_allocator as its backing allocator.

The arena gives you many fast temporary allocations. The page allocator supplies the larger memory blocks behind the arena.

A Simple Arena Example

const std = @import("std");

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();

    const allocator = arena.allocator();

    const a = try allocator.dupe(u8, "red");
    const b = try allocator.dupe(u8, "green");
    const c = try allocator.dupe(u8, "blue");

    std.debug.print("{s}, {s}, {s}\n", .{ a, b, c });
}

In this code, you do not free a, b, and c one by one.

The arena owns those allocations. When arena.deinit() runs, it releases its memory back through the page allocator.

When Page Allocator Is Useful

Use the page allocator when you want a simple allocator and do not need leak detection or complex allocation behavior.

It is useful for:

simple examples
short-lived programs
backing an arena
large allocations
low-level memory experiments

For beginner applications, GeneralPurposeAllocator is often better because it can help catch leaks.

For temporary grouped allocations, ArenaAllocator backed by page_allocator is often clear and simple.

Large Allocations

The page allocator can be reasonable when the allocation is large.

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    const megabyte = 1024 * 1024;
    const buffer = try allocator.alloc(u8, megabyte);
    defer allocator.free(buffer);

    @memset(buffer, 0);

    std.debug.print("allocated {d} bytes\n", .{buffer.len});
}

A large allocation fits the page allocator better than many small allocations.

But the same rule remains:

If you allocate it, you need a cleanup plan.

Page Allocator Does Not Detect Leaks Like GPA

The general purpose allocator can help report leaks when deinitialized.

The page allocator is just a global allocator value. You do not create it and call deinit on it in the same way.

That means it is less useful when you want leak checking.

Compare:

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();

This gives you a point where the allocator can check its internal state.

With:

const allocator = std.heap.page_allocator;

there is no equivalent local gpa.deinit() call for your program’s allocations.

So when learning, prefer this for debugging:

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();

const allocator = gpa.allocator();

Use page_allocator when you specifically want its simplicity or page-level behavior.

Page Size and Waste

If your allocation size does not match page boundaries, the operating system still manages memory in pages.

This can waste memory for small allocations.

For example, conceptually:

You ask for 24 bytes.
The system may need to reserve a page-sized region.
Most of that page is unused by your allocation.

A higher-level allocator can reduce this waste by placing many small allocations inside larger blocks.

That is why general purpose allocators exist.

Page Allocator vs General Purpose Allocator

AllocatorMain RoleGood ForWeakness
Page allocatorAsk OS for memory pagesLarge/simple allocationsInefficient for many small allocations
General purpose allocatorFlexible heap managementMixed allocation sizesMore machinery
Arena allocatorGrouped temporary allocationMany values with same lifetimePoor for independent lifetimes
Fixed buffer allocatorAllocate from fixed memoryStrict memory limitsCannot grow

The page allocator is low-level. It is useful, but it should not be your automatic answer to every allocation problem.

Common Beginner Pattern

For small learning programs, this is acceptable:

const allocator = std.heap.page_allocator;

For code where you want leak checking, use this:

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
    const status = gpa.deinit();
    if (status == .leak) {
        std.debug.print("memory leak detected\n", .{});
    }
}

const allocator = gpa.allocator();

For many temporary allocations with one lifetime, use this:

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

const allocator = arena.allocator();

Each pattern answers a different memory question.

The Core Idea

The page allocator is Zig’s direct way to ask the operating system for memory pages.

It is simple and useful, especially as a backing allocator, but it is usually too low-level for many small allocations.

The rule is:

Use the page allocator when page-level allocation is acceptable, or when another allocator will manage the details above it.