#067 Apr 7, 2026

67. Box::leak — Turn Owned Data Into a Static Reference

Sometimes you need a &'static str but all you have is a String. Meet Box::leak — it deliberately leaks heap memory so you get a reference that lives forever.

The problem

Many APIs demand &'static str — logging frameworks, CLI argument definitions, thread names, and error messages baked into types. But your data is often dynamic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn set_thread_name(name: &'static str) {
    // Imagine this requires a 'static lifetime
    println!("Thread: {name}");
}

fn main() {
    let prefix = "worker";
    let id = 42;
    let name = format!("{prefix}-{id}");

    // This won't compile — name is a String, not &'static str
    // set_thread_name(&name);

    // Workaround: leak it
    let name: &'static str = Box::leak(name.into_boxed_str());
    set_thread_name(name);
}

How it works

Box::leak consumes the Box and returns a mutable reference with a 'static lifetime. The memory stays allocated on the heap but is never freed — it “leaks” on purpose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn main() {
    // Leak a String into &'static str
    let s: &'static str = Box::leak(String::from("hello").into_boxed_str());
    assert_eq!(s, "hello");

    // Leak a Vec into &'static [T]
    let nums: &'static [i32] = Box::leak(vec![1, 2, 3].into_boxed_slice());
    assert_eq!(nums, &[1, 2, 3]);

    // Leak any type into &'static T
    let val: &'static mut i32 = Box::leak(Box::new(99));
    *val += 1;
    assert_eq!(*val, 100);
}

The pattern is always the same: put your data in a Box, then call leak() to trade ownership for a 'static reference.

When it makes sense

Box::leak shines for data that truly lives for the entire program — configuration, lookup tables, or singleton-style values initialized once at startup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
struct Config {
    db_url: String,
    max_connections: usize,
}

fn init_config() -> &'static Config {
    let config = Config {
        db_url: String::from("postgres://localhost/mydb"),
        max_connections: 10,
    };
    Box::leak(Box::new(config))
}

fn main() {
    let config = init_config();

    // Now any function can take &Config without lifetime gymnastics
    assert_eq!(config.max_connections, 10);
    assert!(config.db_url.contains("localhost"));
}

No Arc, no lazy_static!, no lifetime parameters threading through your call stack — just a plain reference.

The catch: you can’t unleak

The memory is genuinely leaked. It won’t be freed until the process exits. This is fine for startup-time singletons but a bad idea in a loop:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn main() {
    // DON'T do this — leaks memory every iteration
    // for i in 0..1000 {
    //     let s: &'static str = Box::leak(format!("item-{i}").into_boxed_str());
    // }

    // If you need to reclaim the memory, you can "unleak" with Box::from_raw:
    let leaked: &'static mut String = Box::leak(Box::new(String::from("temporary")));
    let ptr: *mut String = leaked;

    // SAFETY: ptr came from Box::leak and hasn't been freed
    let reclaimed: Box<String> = unsafe { Box::from_raw(ptr) };
    assert_eq!(*reclaimed, "temporary");
    // reclaimed is dropped here — memory freed
}

Use Box::leak when the data truly needs to outlive everything. For anything else, reach for Arc, OnceLock, or scoped lifetimes instead.

← Previous 66. Inferred Const Generics — Let the Compiler Count For You Next → 68. f64::next_up — Walk the Floating Point Number Line