Memory Management
March 13, 2026 · View on GitHub
Zigly functions that allocate memory require an explicit allocator. This document covers allocator patterns for edge services.
Why Explicit Allocators?
Zig doesn't have a global allocator. Functions that need to allocate memory take an Allocator parameter:
// Allocates memory for the header value
const user_agent = try request.headers.get(allocator, "User-Agent");
This makes allocation explicit and controllable, which matters in memory-constrained WebAssembly environments.
Allocator Types
Page Allocator
The simplest choice for direct memory allocation:
const allocator = std.heap.page_allocator;
const data = try allocator.alloc(u8, 1024);
defer allocator.free(data);
On WebAssembly targets, page_allocator automatically uses WasmAllocator under the hood. This is a WASM-optimized allocator that:
- Uses 64KB bigpages matching WASM memory page size
- Maintains free lists for efficient memory reuse
- Grows memory via
@wasmMemoryGrow
This means you get WASM-optimized allocation without changing your code. The allocator is fast but doesn't track allocations for leak detection.
Note: std.heap.wasm_allocator is identical to page_allocator on WASM targets. Using page_allocator is preferred because it keeps your code portable for native testing.
Debug Allocator
Tracks allocations and detects leaks in debug builds:
var debug_alloc = std.heap.DebugAllocator(.{}).init(std.heap.page_allocator);
defer {
const status = debug_alloc.deinit();
if (status == .leak) {
std.debug.print("Memory leak detected!\n", .{});
}
}
const allocator = debug_alloc.allocator();
Slower than page allocator but helps find memory issues during development.
Arena Allocator
Allocates from a growing region, frees everything at once:
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // Frees all allocations
const allocator = arena.allocator();
// These allocations are freed together when arena is deinitialized
const header1 = try request.headers.get(allocator, "Host");
const header2 = try request.headers.get(allocator, "User-Agent");
const body = try request.body.readAll(allocator, 0);
// No individual free() calls needed
Arenas are ideal for request handling—allocate throughout the request, free everything at the end.
Patterns
Request-Scoped Arena
The recommended pattern for most services:
fn start() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var downstream = try zigly.downstream();
// All allocations use the arena
const user_agent = try downstream.request.headers.get(allocator, "User-Agent");
const body = try downstream.request.body.readAll(allocator, 0);
// Process request...
try downstream.response.setStatus(200);
try downstream.response.finish();
}
// Arena frees everything when function returns
Scoped Allocations
Use inner arenas for temporary work:
fn start() !void {
var main_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer main_arena.deinit();
const allocator = main_arena.allocator();
var downstream = try zigly.downstream();
// Temporary processing with inner arena
{
var temp_arena = std.heap.ArenaAllocator.init(allocator);
defer temp_arena.deinit();
const temp = temp_arena.allocator();
const large_data = try fetchLargeData(temp);
const result = processData(large_data);
// large_data freed here when temp_arena is deinitialized
}
// Continue with main allocator
}
Fixed Buffers
For known-size data, use stack buffers:
fn start() !void {
var downstream = try zigly.downstream();
// Stack buffer for method (methods are short)
var method_buf: [16]u8 = undefined;
const method = try downstream.request.getMethod(&method_buf);
// Stack buffer for URI
var uri_buf: [4096]u8 = undefined;
const uri = try downstream.request.getUriString(&uri_buf);
// No allocation needed for these
}
Memory-Aware Functions
Some Zigly functions allocate, others use provided buffers:
// ALLOCATES - returns owned memory
const header = try request.headers.get(allocator, "Host"); // allocator required
// BUFFER - writes to provided buffer
var method_buf: [16]u8 = undefined;
const method = try request.getMethod(&method_buf); // no allocator
// BUFFER - writes to provided buffer
var uri_buf: [4096]u8 = undefined;
const uri = try request.getUriString(&uri_buf); // no allocator
Prefer buffer-based functions when you know the maximum size.
Reading Bodies
Bodies can be large. Control memory usage:
// Read entire body (for small bodies)
const body = try request.body.readAll(allocator, 0); // 0 = no limit
// Limit maximum size
const body = try request.body.readAll(allocator, 1024 * 1024); // 1MB max
// Streaming read (for large bodies)
var buf: [4096]u8 = undefined;
while (true) {
const chunk = try request.body.read(&buf);
if (chunk.len == 0) break;
// Process chunk...
}
WebAssembly Memory Limits
WebAssembly linear memory starts small and grows on demand. Be aware of:
- Initial memory: Usually 1-4 pages (64KB each)
- Maximum memory: Platform-dependent, typically 256MB-4GB
- Growth: Memory can grow but never shrink
- Page size: WASM pages are always 64KB
The WasmAllocator (used by page_allocator on WASM) is optimized for this model, using 64KB bigpages that align with WASM memory pages.
Large allocations can fail if memory can't grow:
const huge = allocator.alloc(u8, 100 * 1024 * 1024) catch |err| {
// Handle out of memory
return err;
};
Avoiding Leaks
Always Defer Cleanup
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // Always runs, even on error
Close Resources
var entry = try cache.lookup("key", .{});
defer entry.close() catch {};
var body = try entry.getBody(null);
defer body.close() catch {};
Debug Allocator for Development
Use DebugAllocator during development to detect leaks:
fn start() !void {
var debug_alloc = std.heap.DebugAllocator(.{}).init(std.heap.page_allocator);
defer {
const status = debug_alloc.deinit();
if (status == .leak) {
std.debug.print("Memory leak detected!\n", .{});
}
}
const allocator = debug_alloc.allocator();
// ...
}
The leak detection only works locally (with std.debug.print), not in production.
Performance Tips
- Use arenas for request-scoped allocations
- Reuse buffers for repeated operations
- Limit body sizes to prevent memory exhaustion
- Prefer stack buffers for small, fixed-size data
- Stream large bodies instead of loading entirely
Example: Efficient Request Handler
const std = @import("std");
const zigly = @import("zigly");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const alloc = arena.allocator();
var downstream = try zigly.downstream();
// Stack buffers for small data
var method_buf: [16]u8 = undefined;
const method = try downstream.request.getMethod(&method_buf);
var uri_buf: [4096]u8 = undefined;
const uri = try downstream.request.getUriString(&uri_buf);
// Arena for variable-size data
const headers = try downstream.request.headers.names(alloc);
// Limit body size
const body = try downstream.request.body.readAll(alloc, 64 * 1024); // 64KB max
// Process and respond
try downstream.response.setStatus(200);
try downstream.response.finish();
}
// Arena frees everything when main() returns
Next Steps
- Architecture - Understand the runtime environment
- Error Handling - Handle allocation failures