---
title: How to Avoid Memory Leaks in Rust (Yes, They're Possible)
summary: Avoid Rust memory leaks by preventing reference cycles with Weak pointers and avoiding intentional memory leaks via std::mem::forget.
lang: en
slug: how-to-avoid-memory-leaks-in-rust-yes-theyre-possible
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-avoid-memory-leaks-in-rust-yes-theyre-possible/
---

# How to Avoid Memory Leaks in Rust (Yes, They're Possible)



## When the memory stays

You're building a scene graph for a game engine. Every node holds a reference to its children, and every child holds a reference back to its parent. You delete the root node, expecting the whole tree to vanish. The profiler shows the memory staying allocated. You check the code. No `malloc` calls, no manual `free`. Rust promised no leaks, but the bytes are sitting there, refusing to move.

This is a memory leak. In Rust, this shouldn't happen, right? It can. Rust prevents memory safety bugs like buffer overflows, use-after-free, and double-free. It does not prevent memory leaks. A leak is safe. It is just inefficient. The program will not crash. It will not invoke undefined behavior. It will simply consume memory until the process ends or the system runs out of resources.

Rust's ownership system usually prevents leaks by design. When a variable goes out of scope, the compiler generates code to drop it. If that variable owns other data, those get dropped too. The chain reaction cleans everything up. Leaks happen when you break this chain. You can break it intentionally by telling the compiler to ignore cleanup. Or you can break it accidentally by creating a reference cycle where two pieces of data hold strong references to each other, keeping each other alive forever.

## Safety does not mean no leaks

Memory safety and memory leak freedom are different guarantees. Memory safety means you cannot access invalid memory. Leak freedom means you cannot lose track of valid memory. Rust guarantees the first. It does not guarantee the second.

Think of a leak as a book left on a table that no one ever reads. The library still owns the book. The book is safe. No one is tearing pages out. But the book is not on the shelf where it belongs. It occupies space. If enough books get left on tables, the library runs out of room.

Rust allows leaks because preventing all leaks would require the compiler to solve the halting problem or track complex graph structures at compile time. That is computationally intractable. Instead, Rust gives you tools to manage shared ownership and lets you decide when to break cycles. The compiler enforces that you cannot accidentally use freed memory. It does not enforce that you free all memory.

**Trust the borrow checker for safety, but trust your profiler for leaks.**

## The intentional leak: forget and ManuallyDrop

The simplest way to leak memory in Rust is to ask for it. The standard library provides `std::mem::forget`. This function takes ownership of a value and does nothing. The value is moved into the function, the function body is empty, and the value is lost. The compiler sees the value moved, so it does not generate drop code for the original variable. The heap allocation remains reachable by no one, but the operating system does not know it is garbage.

```rust
use std::mem::forget;

fn main() {
    // Allocate a String on the heap.
    let data = String::from("This will leak");
    
    // forget takes ownership and suppresses the drop glue.
    // The String's memory is never freed.
    forget(data);
    
    // The program continues. The memory is leaked.
    // When main exits, the OS reclaims everything,
    // but during runtime the leak existed.
}
```

Calling `forget` directly is rare in production code. The community prefers `std::mem::ManuallyDrop` when you need to control drop timing. `ManuallyDrop` is a type wrapper that signals intent. It prevents the value from being dropped automatically, but it keeps the value accessible. You can drop it manually later, or you can leak it intentionally. The type system reminds readers that the value is not being dropped.

```rust
use std::mem::ManuallyDrop;

fn main() {
    // ManuallyDrop wraps the value and suppresses automatic drop.
    let data = ManuallyDrop::new(String::from("Controlled leak"));
    
    // You can still read the value via Deref.
    println!("Data: {}", data);
    
    // If you never call ManuallyDrop::into_inner,
    // the value leaks when data goes out of scope.
    // This is explicit and visible in the type signature.
}
```

**Convention aside:** Use `ManuallyDrop` instead of `forget` when you want to signal that a value will not be dropped automatically. `forget` is a blunt instrument. `ManuallyDrop` is a type that documents your intent. If you see `ManuallyDrop` in a signature, you know the caller is responsible for cleanup.

## The accidental leak: reference cycles

Intentional leaks are obvious. Accidental leaks are harder to spot. They happen with reference-counted smart pointers like `Rc<T>` and `Arc<T>`. These types allow multiple owners of the same data. They keep a counter of how many owners exist. When the counter hits zero, the data is dropped.

A leak occurs when you create a cycle. If A points to B and B points to A, both counters are at least one. Neither counter hits zero. Neither value drops. The memory stays allocated until the process ends.

```rust
use std::rc::Rc;

struct Node {
    value: i32,
    // Strong reference to the next node.
    next: Option<Rc<Node>>,
}

fn main() {
    let node1 = Rc::new(Node { value: 1, next: None });
    let node2 = Rc::new(Node { value: 2, next: Some(node1.clone()) });
    
    // Create a cycle: node1 points to node2, node2 points to node1.
    // This requires interior mutability to modify node1 after creation.
    // For brevity, assume we can mutate next.
    // In practice, this cycle prevents both nodes from dropping.
    // The ref counts stay at 1. Memory leaks.
}
```

The compiler does not warn about cycles. The borrow checker tracks ownership lifetimes, not reference counts. It cannot prove that a cycle exists at compile time. You need runtime tools to find these leaks. Tools like `valgrind` or `cargo-leak` can detect memory that was allocated but never freed.

**The borrow checker won't warn you about leaks. Profile your code if you use `Rc`.**

## Breaking the cycle with Weak

The solution to reference cycles is `Weak` pointers. `Weak<T>` is a non-owning reference to a value managed by `Rc<T>` or `Arc<T>`. It points to the same allocation but does not increment the strong reference count. It is a "maybe" pointer. You can upgrade it to an `Rc` if you need to use the value, but the upgrade might fail if the value has been dropped.

In a graph, you use `Rc` for parent-to-child references and `Weak` for child-to-parent references. The parent owns the child. The child knows about the parent but does not keep it alive. When the parent is dropped, the child's `Weak` reference becomes invalid. The cycle is broken.

```rust
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    // Strong reference to child. Parent owns child.
    next: Option<Rc<RefCell<Node>>>,
    // Weak reference to parent. Child does not own parent.
    parent: Weak<RefCell<Node>>,
}

fn main() {
    // Create parent node.
    let parent = Rc::new(RefCell::new(Node {
        value: 1,
        next: None,
        parent: Weak::new(),
    }));

    // Create child node with a Weak reference to parent.
    let child = Rc::new(RefCell::new(Node {
        value: 2,
        next: None,
        // Downgrade the Rc to a Weak.
        // This does not increment the strong count.
        parent: Rc::downgrade(&parent),
    }));

    // Parent holds strong ref to child.
    parent.borrow_mut().next = Some(child.clone());

    // When parent and child go out of scope:
    // 1. child drops. Its ref count decreases.
    // 2. parent drops. Its ref count decreases.
    // 3. child's ref count hits zero. Child is freed.
    // 4. parent's ref count hits zero. Parent is freed.
    // No cycle. No leak.
}
```

`Weak` pointers require careful handling. You cannot use a `Weak` pointer directly. You must call `upgrade()` to get an `Option<Rc<T>>`. If the target is still alive, you get `Some(Rc)`. If the target was dropped, you get `None`. This prevents dangling pointers. The compiler enforces that you check the `Option` before using the value.

```rust
// Accessing the parent from the child.
let parent_opt = child.borrow().parent.upgrade();
match parent_opt {
    Some(p) => println!("Parent value: {}", p.borrow().value),
    None => println!("Parent was dropped"),
}
```

**Convention aside:** Use `Rc::clone(&rc)` instead of `rc.clone()` when cloning an `Rc`. Both compile, but `Rc::clone` makes it clear that you are cloning the pointer, not the data. `rc.clone()` looks like a deep clone to readers who are not familiar with `Rc`. The explicit form reduces cognitive load.

**Break the cycle with `Weak`. If every pointer is strong, nothing ever dies.**

## Pitfalls and silent failures

Memory leaks in Rust are silent. The compiler does not emit errors. The program does not panic. You only notice when memory usage grows over time. This makes leaks insidious. They can hide in long-running processes like servers or daemons. A small leak per request can exhaust memory after thousands of requests.

Common pitfalls include:

- Forgetting to use `Weak` for back-references in graphs or trees.
- Using `Rc` in a cycle and assuming the compiler will catch it.
- Holding `Rc` references in global state or caches without eviction policies.
- Using `forget` or `ManuallyDrop` and forgetting to drop the value manually.

The compiler error `E0382` (use of moved value) might appear if you try to move a value out of a borrowed context while debugging a leak. This error is unrelated to the leak itself. It indicates a move violation. Leaks do not produce compiler errors. You must rely on testing and profiling.

**The compiler guarantees safety, not efficiency. Leaks are safe but wasteful.**

## Choosing the right pointer

Shared ownership adds overhead and complexity. Use it only when necessary. Prefer plain ownership when possible. When you need shared ownership, choose the right tool for the job.

Use `Rc<T>` for single-threaded shared ownership when the data structure is a tree or a DAG with no cycles. Use `Arc<T>` for multi-threaded shared ownership when the data is read from multiple threads. Use `Weak<T>` for back-references in graphs or caches where the reference should not keep the target alive. Use `ManuallyDrop<T>` when you need to suppress automatic drop and signal intent to readers. Use `std::mem::forget` only when implementing custom `Drop` logic or interfacing with FFI functions that take ownership. Reach for plain ownership when possible; shared ownership is a tool, not a default.

**Prefer ownership. Shared ownership is a tool, not a default.**

## Where to go next

- [How Does Ownership Work with Structs in Rust?](/en/how-does-ownership-work-with-structs-in-rust/)
- [What is the drop order in Rust](/en/what-is-the-drop-order-in-rust/)
- [What is the Drop trait](/en/what-is-the-drop-trait/)