When individual drops get in the way
You are writing a parser for a configuration file. The file has ten thousand lines. Your code creates a node for every line, links them together, and builds a tree. When the parse finishes, you drop the tree. Rust calls drop on every single node. The runtime walks the tree, frees memory, and spends time doing cleanup work you do not need. You just want to wipe the slate clean and start parsing the next file.
Standard allocation gives you fine-grained control. You allocate a node, you free a node. That control has a cost. Every allocation requires searching for free space, updating bookkeeping structures, and potentially calling the operating system. Every deallocation requires reversing that work. When you create thousands of objects that all live for the same amount of time, that overhead adds up.
Arena allocation trades individual control for bulk speed. You allocate many objects from a single block of memory. You never free them one by one. When you are done, you free the entire block at once. The cost of freeing ten thousand objects is the same as freeing one.
The arena concept
Think of a hotel where you check in guests by handing them a key. When they leave, you have to clean the room, reset the bed, and update the registry. Now imagine a conference hall. You hand out name badges from a stack. When the conference ends, you do not clean individual rooms. You just lock the doors and reset the hall for tomorrow.
Arena allocation is the conference hall. You allocate from a big block of memory. You never free individual items. When the arena drops, the whole block vanishes.
This pattern shines when objects share a lifetime. If everything you allocate lives until a specific event, an arena lets you manage that lifetime in one place. The community standard for this in Rust is the bumpalo crate. It implements a bump allocator, which is the simplest and fastest form of arena.
Trade individual control for bulk speed.
Minimal example
Add bumpalo to your Cargo.toml and create a Bump instance. Use alloc to place values into the arena. The allocator returns references, not owned values.
use bumpalo::Bump;
fn main() {
// Create the arena. This reserves a chunk of memory.
let bump = Bump::new();
// Allocate a value. This returns a reference, not an owned value.
// The reference lives as long as `bump` exists.
let x: &i32 = bump.alloc(42);
// Allocate another value. The pointer just moves forward.
let y: &str = bump.alloc("hello");
// Use the references normally.
println!("x = {}, y = {}", x, y);
// When `bump` goes out of scope, all memory is reclaimed instantly.
// No individual drop calls happen for `x` or `y`.
}
The pointer bump is all the work the allocator does.
How the bump allocator works
When you call Bump::new, the allocator asks the OS for a large block of memory. It keeps a cursor pointing to the start of that block. When you call bump.alloc(value), the allocator copies value to the cursor position, moves the cursor forward by the size of value, and returns a reference to that spot.
There is no search for free space. There is no bookkeeping for individual allocations. The cost is just copying the value and moving a pointer. This operation is often a single machine instruction.
When bump drops, the allocator returns the entire block to the OS. The cost of freeing everything is constant, regardless of how many items you allocated.
This approach also improves cache locality. Allocations are contiguous in memory. When you iterate over objects in the arena, the CPU loads them into cache efficiently. Standard heap allocations scatter objects across the heap, causing cache misses.
Arena allocation turns allocation overhead into a fixed cost.
Reusing the arena
If you run the same allocation pattern in a loop, creating a new arena every time wastes time. You can reset the arena to reuse the memory block.
use bumpalo::Bump;
fn main() {
// Create the arena once.
let bump = Bump::new();
for i in 0..1000 {
// Allocate values for this iteration.
let data = bump.alloc(format!("iteration {}", i));
println!("{}", data);
// Reset the cursor. Memory is not returned to the OS.
// The next allocation reuses the same block.
bump.reset();
}
}
reset moves the cursor back to the start. It does not return memory to the OS. It does not run Drop on the old values. It just marks the block as free for reuse. This avoids the cost of requesting new memory from the OS in every loop iteration.
Reset the arena to keep memory hot.
Realistic usage with lifetimes
Arena allocation pairs naturally with Rust's lifetime system. When you allocate into an arena, the returned reference has a lifetime tied to the arena. You can store references to other arena-allocated values without worrying about dangling pointers.
use bumpalo::Bump;
/// A node in a tree that holds references to other nodes.
struct Node<'a> {
/// The value stored in this node.
value: i32,
/// A reference to the parent node, owned by the arena.
parent: Option<&'a Node<'a>>,
}
/// Build a small tree inside the arena.
/// The returned reference lives as long as the arena.
fn build_tree(bump: &Bump) -> &Node<'_> {
// Allocate the root. The lifetime is tied to `bump`.
let root = bump.alloc(Node {
value: 1,
parent: None,
});
// Allocate a child. It can hold a reference to the root.
// Both live in the same arena, so the reference is always valid.
let child = bump.alloc(Node {
value: 2,
parent: Some(root),
});
// Return the child. The caller gets a reference that lives as long as the arena.
child
}
fn main() {
let bump = Bump::new();
let tree = build_tree(&bump);
println!("Child value: {}, Parent value: {}", tree.value, tree.parent.unwrap().value);
}
Functions that allocate into the arena usually take bump: &Bump as the first argument. This is a community convention. It keeps the arena accessible without moving it. The variable name bump is also standard. When you see bump.alloc, you know exactly what is happening.
Lifetimes in Rust make this pattern safe by default.
Pitfalls and compiler errors
Arena allocators skip individual drops. If you allocate a type that implements Drop, the destructor never runs. This is safe for data-only types, but dangerous for resources. If you allocate a file handle, a mutex, or a network socket into an arena, the resource leaks. The arena frees the memory, but it does not call drop on the value.
If your type needs cleanup, the arena is the wrong tool.
You cannot return a reference from a function where the arena is local. The compiler enforces this strictly.
fn bad() -> &i32 {
let bump = Bump::new();
bump.alloc(42)
// Error: E0515. `bump` is dropped at the end of the function.
// The reference would dangle.
}
The compiler rejects this with E0515 (returned value does not live long enough). The arena must outlive the references you return. Pass the arena as a parameter or return the arena itself.
bump.alloc can panic if the arena runs out of memory. In release builds, this is a hard panic. If you need graceful failure, check the crate documentation for error-returning variants. Most high-performance code accepts the panic, since out-of-memory is a fatal condition anyway.
Check your types for Drop before using the arena.
Decision: arena versus standard allocation
Use arena allocation when you create many short-lived objects and want to free them all at once. Use arena allocation when you need fast allocations and can tolerate skipping individual Drop calls. Use arena allocation when building graphs or trees where nodes hold references to each other, and you want to avoid complex lifetime management. Reach for Box or Vec when you need to free items individually or when objects have varying lifetimes. Reach for Rc or Arc when you need shared ownership and do not know the lifetime structure ahead of time.
Pick the tool that matches your lifetime shape.