How to Use Box<T> for Heap Allocation in Rust

Use Box::new(value) to allocate data on the heap for recursive types or large data transfers.

When the stack isn't enough

You are building a configuration parser. You read a 50MB JSON file and deserialize it into a Config struct. You pass that struct to a function. The compiler copies the entire 50MB onto the stack. Your program crashes with a stack overflow before it even starts.

Or you are defining a tree structure where a node contains other nodes. The compiler stops you immediately. It cannot figure out how big the type is because the type contains itself, which contains itself, forever.

Both problems point to the same solution. You need to move the data off the stack and onto the heap, and keep a small handle to it on the stack. That is what Box<T> does.

The box is a handle to the heap

Rust puts most variables on the stack. The stack is fast and automatic, but it has strict limits. It is like a workbench: you can only have a few tools out at once, and they have to be small enough to fit on the surface. The heap is the warehouse. It is huge, but you have to manage access to it.

Box<T> is a pointer. It lives on the stack, but it points to data on the heap. Think of it like a key to a storage unit. The key is tiny and fits in your pocket. The storage unit can hold a sofa. When you pass the key to a friend, you are not moving the sofa. You are just giving them access to the unit.

Box<T> gives you heap allocation with the same ownership rules as stack variables. The compiler still tracks who owns the box, and it cleans up the heap memory when the box goes out of scope. You get the flexibility of the heap with the safety of the stack.

The box is the handle. The heap is the payload. Keep the handle small.

Minimal example

Here is the simplest way to use a box. You wrap a value with Box::new, and the value moves to the heap.

fn main() {
    // Allocate an integer on the heap.
    // b is a pointer on the stack pointing to the heap data.
    let b = Box::new(42);

    // Access the value inside the box.
    // Rust automatically dereferences the box here.
    println!("Value inside the box: {}", b);
}

The variable b holds a pointer. The integer 42 lives on the heap. When you print b, Rust does not print the memory address. It prints the value inside. This happens because Box implements the Deref trait, which tells the compiler how to treat the box like the value it points to.

Convention: You rarely need to write *b explicitly. The compiler handles dereferencing for you. Writing *b is usually only needed when you want to move the value out of the box or when the compiler cannot infer the deref path. Trust the auto-deref.

How the compiler handles the box

When you call Box::new(value), the compiler generates code to request memory from the heap allocator. It copies the value into that memory and returns a pointer. The pointer is the Box.

Moving a Box is always cheap. If you have a Box containing a 1GB array, moving the Box only copies the pointer. The pointer is the size of a memory address, typically 8 bytes on modern systems. The 1GB of data stays exactly where it is on the heap.

This is a massive difference from stack values. If you move a large struct on the stack, the compiler copies every byte. If you box the struct, you only copy the pointer.

If you forget this and try to use the box after moving it, the compiler rejects you with E0382 (use of moved value). The box follows the same move semantics as any other owned value. Once you move the box, the original variable is gone.

Recursive types and infinite size

The most common reason to reach for Box is recursive types. The compiler needs to know the size of every type at compile time. This allows it to allocate stack space and calculate offsets. A type that contains itself would be infinitely large. The compiler cannot handle that.

#[derive(Debug)]
enum List {
    // A node holds a value and a pointer to the next node.
    // Without Box, the compiler cannot calculate the size.
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    // Build a small list: 1 -> 2 -> Nil.
    let list = List::Cons(
        1,
        Box::new(List::Cons(
            2,
            Box::new(List::Nil),
        )),
    );

    println!("{:?}", list);
}

If you remove the Box from the Cons variant, the compiler halts with E0072 (recursive type has infinite size). The Box breaks the cycle. Box<List> has a known size: the size of a pointer. The compiler can calculate the size of List because it only needs to account for the pointer, not the entire nested structure.

The data structure still exists on the heap. Each node is allocated separately. The Box provides the link between nodes while keeping the type size finite.

Without the box, the type is infinite. With the box, it is just a pointer. Trust the pointer to break the cycle.

Pitfalls and performance costs

Box is not free. Heap allocation takes time. It is slower than stack allocation because the allocator has to find free memory, track it, and handle fragmentation. Accessing data through a box also breaks cache locality. The CPU cache is optimized for data that sits close together on the stack. When you follow a pointer to the heap, the data might be far away, causing a cache miss.

Do not box small values just because you can. If you have a 16-byte struct, keeping it on the stack is faster. Box it only when you need to transfer ownership of large data, or when you need indirection for recursive types or trait objects.

Another pitfall is confusing Box with references. A reference (&T) borrows data. A Box owns data. If you only need to read or modify data temporarily, use a reference. References are zero-cost. They do not allocate memory, and they do not move data. Using a Box when a reference would do is a performance mistake.

Convention: When you see Box<dyn Trait>, read it as "I own a value that implements this trait, and I do not care about the concrete type." This is the standard way to achieve polymorphism in Rust. The community expects trait objects to be boxed because they need a fixed size for the vtable pointer. The box provides that fixed size while allowing the underlying type to vary.

Don't box everything. If a reference works, use it. Allocation is a cost you pay every time.

Decision: Box vs references vs other pointers

Use Box<T> when you need heap allocation for a large value and want to transfer ownership. Use Box<T> for recursive types where the compiler needs a fixed-size pointer to break the cycle. Use Box<dyn Trait> when you need dynamic dispatch and want to own the trait object.

Reach for plain references (&T or &mut T) when you only need to read or modify data temporarily without taking ownership. References are zero-cost and the compiler handles the lifetime checks.

Pick Rc<T> when multiple parts of your program need to read the same data and you cannot determine a single owner at compile time. Rc adds reference counting overhead, so use it only when shared ownership is necessary.

Pick Vec<T> when you need a growable array of values. Vec manages a contiguous block of memory on the heap and handles resizing for you. Use Box for a single value, Vec for a collection.

Pick the tool that matches the ownership model. Ownership needs a box. Borrowing needs a reference. Sharing needs a count.

Where to go next