Skip to content
← All articles

'static has two completely different meanings

`T: 'static` is the single most misread bound in Rust. It does not mean "lives forever" — it means "does not borrow anything short-lived", and every owned type satisfies it. Sorting this out unblocks threads, trait objects and error handling in one go.

A learner sees thread::spawn require F: Send + 'static, concludes that you can only spawn threads over string literals, and quietly gives up on concurrency. The same misreading blocks trait objects and error handling.

It is entirely a vocabulary problem, and it is this: 'static means two different things depending on where it appears.

Meaning one: &'static T — a reference

let s: &'static str = "hello";

This is a reference whose referent is valid for the entire program. It is the ordinary lifetime story with the largest possible region. You get one from:

  • String literals"hello" is &'static str, baked into the binary.
  • const and static items — their data lives for the program’s duration.
  • Box::leak — a heap allocation you deliberately never free.

Nothing surprising here. &'static T is a promise about where the data lives.

Meaning two: T: 'static — a bound

fn needs_static<T: 'static>(t: T) { .. }

This is a constraint on a type, and it means something completely different:

“This type contains no references that could become invalid.”

Equivalently: every lifetime parameter inside T is 'static, or T has no lifetime parameters at all. Which means every owned type satisfies it:

String: 'static          // yes
Vec<u8>: 'static         // yes
i64: 'static             // yes
(String, Vec<f64>): 'static  // yes
&'static str: 'static    // yes
&'a str: 'static         // NO, unless 'a is 'static
Vec<&'a u8>: 'static     // NO

Say it once more, because it is the whole article:

T: 'static does not mean “lives forever”. It means “does not borrow anything short-lived”.

A String you allocate at noon, mutate at 12:01 and drop at 12:02 satisfies T: 'static for its entire existence — because at no point does it point at anybody else’s data.

fn needs_static<T: 'static>(_t: T) {}

fn main() {
    let s = String::from("noon");
    needs_static(s);           // compiles. `s` is dropped moments later.
}
💡thread::spawn requires F: Send + 'static. Given the definition above, explain in one sentence what that actually rules out — and what it emphatically does not. click to reveal

It rules out closures that capture references to anything the spawning thread might drop. It does not rule out closures that capture owned data.

let s = String::from("hi");
let h = std::thread::spawn(move || println!("{s}"));   // fine
h.join().unwrap();

The move transfers ownership of s into the closure. The closure’s type now contains a String, which is 'static by the definition, so the bound is satisfied — even though s will be dropped when the thread finishes, milliseconds later.

Why the bound is needed at all: a spawned thread can outlive the function that spawned it (nothing forces you to join). If the closure held a &'a str pointing into a local, that local could be freed while the thread is still reading it. F: 'static is exactly the statement “there is no such reference in here”.

And the reason it is not the end of the story: std::thread::scope exists precisely so you can borrow across threads, by guaranteeing structurally that the scope does not end until every spawned thread has finished. The 'static bound is the price of unstructured spawning, not a law about threads.

The third sighting: const and static items

Reference types in const and static declarations get an implicit 'static:

const GREETING: &str = "hi";      // means &'static str
static NAME: &str = "world";      // likewise

Which is why “helpfully” writing it out gets you told off:

const G: &'static str = "hi";
error: constants have by default a `'static` lifetime
  help: consider removing `'static`: `&str`
  = note: `-D clippy::redundant-static-lifetimes` implied by `-D warnings`

clippy::redundant_static_lifetimes is warn-by-default and therefore fatal on this site. Do not annotate your consts.

The fourth sighting: trait objects

Box<dyn Error>            // means Box<dyn Error + 'static>
Box<dyn Fn(i64) -> i64>   // means Box<dyn Fn(i64) -> i64 + 'static>

Trait objects have default lifetime bounds, and because Box<T> imposes no lifetime bound on T, the default is 'static. There is no warning and no visual cue. This is the source of a whole family of confusing errors — a boxed closure that captures a local reference fails with a message about 'static that mentions nothing you wrote. It gets its own article; for now, just know that a bare dyn Trait inside a Box is silently + 'static, and dyn Trait + '_ is how you opt out.

💡Which of these compile? For each failure, say which of the two meanings is involved. click to reveal
fn a<T: 'static>(_t: T) {}
fn b(_s: &'static str) {}

fn main() {
    let owned = String::from("x");
    let literal = "y";
    let borrowed: &str = &owned;

    a(owned.clone());   // 1
    a(borrowed);        // 2
    b(literal);         // 3
    b(borrowed);        // 4
}

1 compiles. String: 'static — an owned type with no lifetime parameters. The bound meaning.

2 fails. borrowed has type &'x str where 'x is the region of the borrow of owned. For T: 'static to hold, 'x would have to be 'static, and it is not — you get error[E0597]:owneddoes not live long enough. The bound meaning: this type does contain a short-lived reference.

3 compiles. "y" really is &'static str. The reference meaning, satisfied honestly.

4 fails. Same as 2, but arriving through the reference meaning: b demands a reference whose referent lives for the whole program, and owned does not.

The pair 2/4 is the useful one to sit with. They fail for what is ultimately the same reason — a short-lived borrow cannot pretend to be a long-lived one — but 2 gets there through a bound on a type and 4 through a lifetime on a reference. Learners who have collapsed the two meanings into one cannot explain why 1 works and 2 does not, and that is the exact gap this article exists to close.

One more thing that exists

&'static mut T is a real type, and it is not the same as &'static T:

let r: &'static mut String = Box::leak(Box::new(String::from("hi")));
r.push('!');

You get one from Box::leak, which gives up ownership of a heap allocation permanently. It is genuinely useful for one-time startup initialisation and is a memory leak everywhere else. Anyone showing it to you without saying “leak” is skipping the important word.

The card to keep

you see it means
&'static T the referent is valid for the whole program
T: 'static the type borrows nothing short-lived (all owned types qualify)
const X: &str implicitly &'static str — do not annotate it
Box<dyn Tr> implicitly Box<dyn Tr + 'static> — no warning, no cue

And the sentence: it does not mean “lives forever”; it means “does not borrow anything short-lived”.