Why Every String Literal Is `&'static str`
Type this into any Rust program and hover over the binding:
let greeting = "hello";
The inferred type is &'static str. Not &str with some mysterious lifetime — specifically 'static. That is not an accident of syntax: the bytes hello live inside the compiled binary, in a read-only data section that is mapped into the process when it starts and unmapped when it exits. The borrow therefore outlives every local, every allocation, and every function call you will ever make. 'static is simply the name for the lifetime that covers the whole program run.
Reading that correctly matters, because 'static is routinely misexplained. It does not mean "global variable", "immutable", "single copy", or "stored in a particular memory region". It makes exactly one claim: this reference stays valid until the program ends. A value satisfies that claim because it lives in the binary, or because it was boxed and leaked on the heap, or because it is owned and will be kept alive by whoever holds it. All three have the type &'static str where relevant, and the borrow checker only cares that the data will outlive every use.
Once you internalize the one claim, the ergonomics explain themselves:
fn banner(text: &'static str) { println!("{text}"); } // caller may pass any long-lived borrow
let owned: &'static str = Box::leak(String::from("on the heap, forever").into_boxed_str());
let borrowed: &'static str = "from the binary";
A function that stores what you pass must demand a promise no scope can fake, which is why such APIs write it plainly: a background worker that keeps your string needs to know it is still there when the worker runs, possibly seconds after your function returned. spawn-style APIs write Send + 'static on their closures for exactly this reason, and you will meet the full story in T: 'static bounds below.
Where &'static T shows up most often in real code: error types that borrow (Box<dyn Error + 'static> — errors are almost always owned), configuration constants, and trait objects stored in long-lived registries. When you see it, your first question should be: "is this data actually long-lived, or was 'static bolted on to silence a borrow error?" — the second case is a design smell covered in the last chapter of this topic.
One mechanic makes all of this composable: &'static T coerces down to any shorter &'a T. Rust treats a longer-lived reference as a subtype of a shorter-lived one and shortens automatically at assignment, argument, and return sites:
fn short(x: &str) { println!("{x}"); }
let literal: &'static str = "always valid";
short(literal); // &'static str silently becomes &'_ str for this call
let local = String::from("temp");
let short_ref: &str = &local; // borrow of a local: fine for this scope
// let forever: &'static str = &local; // error: `local` has lifetime '2
Direction matters: shortening a 'static borrow is free, while promoting a local borrow to 'static is impossible without changing ownership (leaking, Box::leak, or Arc::clone into something long-lived). Every 'static error you will ever see is the second direction attempted by accident.
A design decision this enables — and interviewers like to probe it — is an API that stores what you pass. A metrics registry that keeps names in a global list has two honest signatures:
fn register_static(name: &'static str); // caller must pass a literal or leaked box
fn register_owned(name: String); // caller pays for an allocation, registry keeps it
The first avoids allocation but locks callers into literals; the second accepts format!("metric_{id}") results. Choosing between them is choosing who owns the data, and 'static in the signature is how Rust makes you state that choice out loud instead of hiding a dangling pointer in a global container.
Declaring `static` and `const` Items Without Getting Sync Errors
'static is a lifetime; static is an item. The names match on purpose, because a static item's storage lasts the whole program run — it has a 'static lifetime when borrowed.
static APP_NAME: &str = "order-service";
static MAX_RETRIES: u32 = 3;
fn describe() -> &'static str { APP_NAME } // borrowing a static yields 'static
A static occupies one fixed address for the entire run. Borrowing it gives a &'static reference, and any thread that can reach that address can read it — which is why the compiler demands T: Sync for any static you share:
static OPEN: std::collections::HashSet<i32> = std::collections::HashSet::new();
// error: `std::collections::HashSet<i32>` cannot be shared between threads safely
// help: consider a thread-safe wrapper or initialization inside a function
A HashSet is not Sync, so a static one is refused. The standard fixes are LazyLock (Rust 1.80+) or its predecessor OnceLock/once_cell::Lazy, which build the value lazily on first access and hand out &T afterwards:
use std::sync::LazyLock;
static ROUTES: LazyLock<std::collections::HashMap<&str, u16>> =
LazyLock::new(|| HashMap::from([("/health", 200), ("/ready", 204)]));
Writing to a static is a different league: static mut COUNTER: u64 = 0; compiles the declaration, but any access to it requires an unsafe block, and unsynchronized access is exactly the data race the type system exists to prevent. Modern Rust replaces it with an AtomicU64 or a Mutex<T>, both of which are safe and, on current hardware, essentially free for infrequent updates.
Contrast all of this with const:
const |
static |
|
|---|---|---|
| Storage | inlined at every use, no fixed address | one address for the whole run |
| Lifetime | value is copied where needed | borrows are &'static |
| Sharing | trivially thread-safe | requires Sync on the type |
| Mutation | never | only via unsafe static mut (avoid) |
| Cost | no memory unless used | occupies memory for the process lifetime |
Use const for numbers and short literals computed at compile time; use static when you need an address — a value that is borrowed, passed as &'static T, or stored in a registry — or an expensive-to-build table initialized once. A const inside a function body behaves like a module-level const: re-evaluated wherever it is used, never borrowed. A static inside a function body, by contrast, is still one global instance shared by every call to that function:
fn hit() -> u64 {
static HITS: AtomicU64 = AtomicU64::new(0); // one counter for the whole program
HITS.fetch_add(1, Ordering::Relaxed) + 1
}
LazyLock<T> deserves a closer look, because it is the tool most production code actually reaches for. It implements Deref<Target = T>, so ROUTES["/health"] works without ceremony; the initialization closure runs exactly once, on first access, and any thread that arrives while it is running blocks until the value is ready — no locks to poison, no initialization races. After initialization, reads cost whatever an &HashMap access costs, and the guard variable itself is a single byte-sized atomic flag plus the table's memory. The lazy path also means a registry that is expensive to build (a parsed routing table, a loaded certificate) costs nothing if the binary never touches it.
Related compile-time tools round out the picture: const NAME: &str = env!("CARGO_PKG_NAME"); reads a value the build system knows, and option_env!("API_TOKEN") bakes in an optional value at compile time, yielding None when the variable was absent. Both produce ordinary const/static items — the point being that "known at compile time" and "stored for the program's lifetime" are orthogonal properties, and const versus static is where they meet.
Reading `T: 'static`: "No Shorter-Lived Borrows", Not "Global"
The bound T: 'static confuses more programmers than any other lifetime syntax, so fix the reading now: T: 'static means every reference inside T is valid for the whole program run. It does not mean T is a global, lives in read-only memory, or is a single shared copy. Any owned value satisfies it, because owned values contain no borrowed data at all:
fn keep<T: 'static>(t: T) { /* ... */ }
keep(String::from("owned")); // ok: String owns its buffer
keep(vec![1_i32, 2, 3]); // ok
keep(Rc::new(42_i32)); // ok: 'static, though NOT Sync/Send
let local = 7_i32;
keep(&local); // error: `local` does not live long enough
That third line is the whole lesson: Rc<i32> is 'static and yet unusable across threads, while &'static i32 would be fine. 'static constrains borrows, not sharing or ownership — sendability is the separate Send/Sync pair.
You meet the bound in earnest with APIs that outlive the current stack frame. Spawning a thread has no idea when your function returns, so the closure must be self-sufficient:
let name = String::from("worker");
std::thread::spawn(move || println!("{name}")); // ok: `name` moved into the closure
let cfg = Config::load();
std::thread::spawn(|| println!("{}", cfg.port)); // error: `cfg` may not live long enough
The standard library spells the requirement as F: FnOnce() + Send + 'static. Owned data moved into the closure satisfies it; a borrow of a local does not, and the fix is to move an owned clone in — not to sprinkle 'static around. The escape hatch when you genuinely want to borrow locals is std::thread::scope, which guarantees joined threads finish before the borrowed data dies and therefore accepts non-'static closures.
Trait objects carry the same default: Box<dyn Fn()> is short for Box<dyn Fn() + 'static>, which is why Box<dyn Fn()>-typed registries reject closures borrowing locals unless you write Box<dyn Fn() + '_> with an explicit lifetime. The same reading settles interview questions about caches, background tasks, and message-passing actors: a Sender<String> demands 'static payloads because a message may be processed long after the sender's scope ends, and owned data is the answer every time.
Trait objects make the default visible, which is worth one careful read. Written alone, dyn Trait means dyn Trait + 'static:
struct Registry { handlers: Vec<Box<dyn Fn(&str) + 'static>> }
impl Registry {
fn add_short<'a>(&mut self, f: Box<dyn Fn(&str) + 'a>) {
self.handlers.push(f); // error: `f` may not live long enough
}
}
The boxed closure must satisfy the vector's 'static requirement, so a closure borrowing a local is refused; you either make closures own their captures or store the trait object with an explicit lifetime tied to the registry, trading a little ergonomics for precision. One exception to the default is worth knowing: behind a reference, the default follows the reference. let r: &dyn Fn(&str) = &f; elides to &'a (dyn Fn(&str) + 'a), so short-lived trait objects can exist — they just cannot be stored in owned containers without a lifetime.
Finally, two 'static questions in one API are not the same question, and separating them is the mark of someone who has thought about it:
let f: Box<dyn Fn(&str) -> &str> = Box::new(|s| s.trim());
The dyn Fn(&str) -> &str part is higher-ranked — the closure must work for every lifetime &str argument it receives, exactly like the for<'a> fn(&'a str) -> &'a str form in Lifetime Elision Rules. The + 'static default on the box is a separate demand: the closure value itself embeds no short borrows. A closure can satisfy the first while violating the second, and the compiler names each failing bound in turn — read them one at a time instead of rewriting the signature until the error moves.
Quiz
1. What is the type of the expression `"orders"`, and why?
2. Why is `static OPEN: HashSet<i32> = HashSet::new();` rejected while `static LIMIT: u32 = 100;` compiles?
3. A function requires `T: 'static`. Which argument satisfies it?
4. `std::thread::spawn(|| println!("{cfg}"))` fails with `cfg may not live long enough`. What are valid repairs?
Flashcards
Question
What does the `'static` lifetime guarantee, and what does it NOT mean?
Click to reveal answer
Answer
It guarantees only that the reference remains valid until the program ends. It does not mean global variable, immutable, single copy, or a specific memory region — data can satisfy it by living in the binary, by being boxed and leaked, or by being owned and kept alive by its holder.
Question
How do `static` items differ from `const` items?
Click to reveal answer
Answer
`static` occupies one address for the whole run, so borrowing it yields `&'static T`, its type must be `Sync` to be shared, and it holds memory for the process lifetime. `const` is inlined at every use, has no address, is never borrowed, and costs nothing when unused.
Question
Why does `static mut` require `unsafe`, and what replaces it?
Click to reveal answer
Answer
Any access to a `static mut` is an `unsafe` block because unsynchronized writes from multiple threads are data races — undefined behavior. Use `AtomicU64` for counters, `Mutex<T>`/`RwLock<T>` for structured data, or `LazyLock`/`OnceLock` for one-time initialization instead.
Question
State the precise meaning of a `T: 'static` bound and give one owned and one borrowed value that fail/succeed it.
Click to reveal answer
Answer
It means every reference inside `T` is valid for the entire program run — i.e. no shorter-lived borrows are embedded. Succeeds: `String`, `Vec<i32>`, `Rc<i32>` (owned, though not Send). Fails: `&local`, `&self.field` from a short borrow — anything holding a non-'static reference.
Question
Why do `thread::spawn` and `Box<dyn Fn()>` require `'static`, and what is the safe alternative to satisfying them with borrows?
Click to reveal answer
Answer
A spawned thread or stored closure may run after the spawning scope ends, so anything it captures must stay valid — hence `F: FnOnce() + Send + 'static`, and `Box<dyn Fn()>` short for `Box<dyn Fn() + 'static>`. Owned data moved in satisfies it; to borrow locals instead, use `std::thread::scope` (joined threads) or write an explicit shorter lifetime like `Box<dyn Fn() + '_>`.
Revision Notes
Key Takeaways
- 1. `'static` means the borrow outlives the program, nothing more — literals, leaked boxes, and owned values all qualify
- 2. String literals are `&'static str` because their bytes sit in the binary's mapped data for the process lifetime
- 3. `static` items have a fixed address and require `Sync` to share; `const` items are inlined and never borrowed
- 4. `T: 'static` forbids embedded short borrows but says nothing about `Send`/`Sync` or uniqueness
- 5. Background threads and stored closures demand `'static` because they outlive your scope — satisfy it with owned data, or use `thread::scope` to keep borrowing
Interview Tips
- • "What does `&'static str` mean?" — "the referenced bytes live for the entire program" plus the crucial clarification: it is a lifetime claim, not a location; a leaked `Box<String>` also produces one
- • "`static` versus `const`?" — address and sharing: statics have one fixed address and need `Sync`, consts are inlined with no storage; mention `LazyLock` for lazy, thread-safe static initialization
- • "Why does `thread::spawn` require `'static`?" — the thread may outlive the spawning scope, so captures must not be borrowed locals; show the fix (move owned data in) and the alternative (`std::thread::scope`)
- • "Is `Rc<i32>` `static`?" — yes, and it is still not `Send`/`Sync`; answering this cleanly shows you separate the borrow-lifetime axis from the thread-safety axis
Cheat Sheet
The 'static Lifetime
One claim only: the reference stays valid until the program ends. Not "global", not "immutable", not "one copy".
Where 'static data comes from:
| Source | Example |
|---|---|
| Binary's data section | "literal", &'static str |
| Box + leak | Box::leak(String::from(s).into_boxed_str()) |
| Owned value kept alive | any String/Vec moved into a long-lived holder |
static vs const:
const |
static |
|
|---|---|---|
| Address | none (inlined) | one, fixed, for the whole run |
| Borrow type | never borrowed | &'static T |
| Sharing | always fine | type must be Sync |
| Write | never | unsafe static mut only — replace with atomics/locks |
| Init | compile time | compile time, or lazily via LazyLock/OnceLock |
T: 'static reads as: "embeds no shorter-lived borrows" — owned data always qualifies; Rc<T> is 'static but not Send.
Typical requirements:
std::thread::spawn(f); // F: FnOnce() + Send + 'static
Box<dyn Fn()> // = Box<dyn Fn() + 'static>
Sender<T>::send(t) // T: Send (payload travels to another thread)
std::thread::scope(|s| ...) // accepts non-'static closures; joins before return