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.