TL;DR: Lifetimes are not about how long a variable lives. They are annotations that tell the compiler how long a reference is valid, allowing it to reject dangling pointer bugs at compile time. Most of the time, the compiler infers lifetimes automatically (lifetime elision). You only write them explicitly when the compiler cannot figure it out from context.
- Lifetimes don't change behavior: they are zero-cost compile-time annotations
- Elision rules handle 90% of cases; you rarely need explicit lifetimes in function signatures
- In structs: if you store a reference, you must annotate the lifetime
'static: means the reference is valid for the entire program; string literals are'static- Common error: "does not live long enough": the referenced data is dropped before the reference
Who Should Read This?
This article is for intermediate Rust developers who have learned the basics of ownership and borrowing but hit a wall when the compiler starts asking for explicit lifetime annotations. If you have encountered errors like "missing lifetime specifier," "does not live long enough," or "lifetime may not live long enough", and the Rust Book's chapter on lifetimes left you with more questions than answers, this guide is designed for you. Lifetimes are the topic that most consistently separates junior Rust engineers from mid-level ones; the engineers who understand them write code that the compiler accepts on the first try rather than fighting the borrow checker. Mid-level Rust engineers fluent in lifetimes in the US earn $145Kâ$190K.
What Is a Lifetime in Rust?
A lifetime is a compile-time label that describes how long a reference remains valid. It prevents dangling references without a garbage collector.
Many developers think lifetimes are about controlling when something is dropped. That's wrong. Lifetimes are purely annotations that help the borrow checker verify that references never outlive the data they point to. No code is generated for lifetimes; they are erased before compilation produces machine code.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}The 'a here says: "the returned reference lives at least as long as both input references." The compiler uses this to verify at every call site that the returned reference is not used after the shorter-lived input is dropped.
Without this annotation, the compiler cannot determine which input's lifetime the return value inherits, and it correctly refuses to guess.
The confusion about lifetimes typically stems from conflating two distinct concepts: the scope of a variable (how long it lives before being dropped) and the lifetime annotation (a label the compiler uses to reason about references). Lifetime annotations do not control drop order. They are descriptions of constraints that already exist; the compiler checks whether your code satisfies those constraints.
Why Do Lifetimes Exist in the First Place?
Lifetimes exist to prevent dangling references (pointers to memory that has been freed) at compile time, without a garbage collector or runtime checks.
Consider what happens in C:
char *get_string() {
char buf[64] = "hello";
return buf; // Undefined behavior: buf is on the stack, freed on return
}This compiles and runs and silently corrupts memory. In Rust, the equivalent is rejected at compile time:
fn get_string() -> &str {
let s = String::from("hello");
&s // ERROR: s does not live long enough: dropped at end of function
}Rust knows s is dropped at the closing }. The returned &str would point to freed memory. The compiler rejects this; no runtime needed.
This is Rust's core safety proposition: the entire class of use-after-free bugs (responsible for a substantial fraction of all CVEs in C and C++ codebases) is eliminated at compile time. The Linux kernel, Android, and the Windows kernel all cite this specific property as the primary motivation for adopting Rust. Lifetimes are the mechanism that makes this guarantee possible.
When Does the Compiler Infer Lifetimes Automatically?
In most function signatures, the compiler applies three elision rules that eliminate the need to write lifetime annotations explicitly. These cover roughly 90% of real-world cases.
Lifetime elision rules (applied in order):
Rule 1: Each reference parameter gets its own distinct lifetime.
fn foo(x: &str, y: &str) -> ...
// becomes internally:
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> ...Rule 2: If there is exactly one reference input parameter, its lifetime is assigned to all output references.
fn first_word(s: &str) -> &str { ... }
// inferred as:
fn first_word<'a>(s: &'a str) -> &'a str { ... }Rule 3: If one of the parameters is &self or &mut self, its lifetime is assigned to all output references.
impl Config {
fn get_name(&self) -> &str { &self.name }
// inferred as:
fn get_name<'a>(&'a self) -> &'a str { &self.name }
}When all three rules apply and fully determine the output lifetimes, you write no annotations. Explicit lifetimes are only required when the rules don't fully resolve the ambiguity; most often when a function takes multiple references and returns one.
Rule 3 is the reason you almost never write lifetime annotations on methods. The vast majority of methods that return a reference return something that came from self, and Rule 3 handles that case automatically. When you do need explicit lifetimes on methods, it is usually because the method takes an additional reference parameter that might be the source of the returned reference.
Bottom line: Lifetime elision covers ~90% of real-world cases. You only need explicit
'aannotations when a function takes multiple reference inputs and returns one, and the compiler can't determine which input the output borrows from.
How Do You Write Explicit Lifetime Annotations?
Lifetime annotations use a tick followed by a name ('a, 'b) placed after & in type positions and declared in angle brackets alongside generic type parameters.
// Single lifetime: return borrows from x
fn first<'a>(x: &'a str, _y: &str) -> &'a str {
x
}
// Shared lifetime: return borrows from whichever input lives shorter
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Multiple lifetimes: different constraints on different parameters
fn mixed<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x // return value only borrows from x, not y
}The lifetime name 'a is just a label: it has no meaning beyond "the compiler uses this to track which reference this output came from." You can use any name, but 'a, 'b, 'c are conventional.
An important subtlety with fn longest<'a>(x: &'a str, y: &'a str) -> &'a str: by using the same lifetime 'a for both parameters and the return value, you are telling the compiler that the returned reference's lifetime is constrained to the shorter of the two input lifetimes. If x lives for 100 lines and y lives for 5 lines, the returned reference is only valid for 5 lines. This is the correct and safe annotation; the compiler will reject any attempt to use the returned reference after the shorter-lived input is dropped.
3 spots open this month â Check if you are eligible.
We help experienced developers transition into Rust roles at âĴ80KââĴ150K+ in Europe or $130Kâ$200K+ in the US.
How Do Lifetimes Work in Structs?
When a struct holds a reference, it must declare a lifetime parameter. This tells the compiler that the struct cannot outlive the data it borrows.
// This struct borrows string data: it doesn't own it
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part // returns from self, so lifetime is elided (Rule 3)
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence;
{
let i = novel.find('.').unwrap_or(novel.len());
first_sentence = &novel[..i];
}
let excerpt = ImportantExcerpt { part: first_sentence };
// excerpt is valid as long as novel is valid
}The 'a on the struct tells the compiler: "an ImportantExcerpt<'a> cannot live longer than the 'a reference it holds." This is checked at every use site.
Structs with lifetime parameters come up frequently when building parsers, tokenizers, and zero-copy data structures. The pattern allows a struct to reference into a buffer or string without copying; this is common in network protocol parsers where you want to process a large binary blob and reference subsections of it without allocating. The lifetime annotation ensures that the parser result struct cannot outlive the input buffer, preventing use-after-free at the type level.
Bottom line: Unless you are building a zero-copy parser or working in a memory-constrained embedded context, prefer owning data in structs (
Stringover&str,Vec<T>over&[T]). The complexity of lifetime-annotated structs rarely justifies the allocation savings in typical application code.
What Is the 'static Lifetime?
'static means the reference is valid for the entire duration of the program. String literals and data embedded in the binary have 'static lifetime.
// String literals are &'static str: they live in the binary
let s: &'static str = "I live in the binary forever";
// Functions can return 'static references
fn always_valid() -> &'static str {
"this string is baked into the compiled binary"
}
// 'static bound on generic types means: the type contains no non-static references
fn spawn_task<T: Send + 'static>(task: T) {
// T must own all its data: no borrowed references allowed
std::thread::spawn(move || { /* use task */ });
}The T: 'static bound does not mean T must live forever; it means T doesn't contain any non-'static references. An owned String satisfies 'static because it owns its data. A &str with a temporary lifetime does not.
The T: 'static bound appears frequently in async Rust. tokio::spawn requires T: Send + 'static because spawned tasks may outlive the scope in which they were created. An async task that borrows a local variable from the spawning scope could access freed memory if the spawning scope ends before the task finishes; the 'static bound prevents this at compile time. The ergonomic solution is almost always Arc<T> (reference-counted ownership shared across the task boundary) rather than trying to find a lifetime that satisfies the constraint.
What Are the Most Common Lifetime Errors and How Do You Fix Them?
"Does not live long enough" and "lifetime mismatch" are the two most common errors. Both are caused by references outliving the data they point to.
Error 1: Data dropped too early
fn get_user_name() -> &str { // ERROR: missing lifetime specifier
let name = String::from("Alice");
&name // name is dropped here: reference would dangle
}
// Fix: return owned data
fn get_user_name() -> String {
String::from("Alice")
}Error 2: Lifetime mismatch in function return
fn longer(x: &str, y: &str) -> &str { // ERROR: ambiguous lifetime
if x.len() > y.len() { x } else { y }
}
// Fix: declare that output borrows from both inputs
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}Error 3: Struct outlives its borrowed data
let excerpt;
{
let novel = String::from("Once upon a time...");
excerpt = ImportantExcerpt { part: &novel };
// novel is dropped here
}
// ERROR: excerpt uses novel after it's dropped
println!("{}", excerpt.part);
// Fix: ensure novel lives at least as long as excerpt
let novel = String::from("Once upon a time...");
let excerpt = ImportantExcerpt { part: &novel };
println!("{}", excerpt.part);Error 4: Non-'static reference in spawned thread
let data = String::from("hello");
let r = &data;
std::thread::spawn(move || {
println!("{}", r); // ERROR: r may not live long enough: thread outlives data
});
// Fix: move the owned data into the thread
let data = String::from("hello");
std::thread::spawn(move || {
println!("{}", data); // data moved into thread, owns it
});Error 1 is the most common mistake for engineers coming from garbage-collected languages. In Python or JavaScript, returning a reference to a locally created object is perfectly safe; the GC keeps the object alive as long as a reference exists. In Rust, the function's stack frame is freed on return, so local references cannot escape. The canonical fix is returning owned data (String instead of &str, Vec<T> instead of &[T]). Only return references when you are genuinely returning a reference to input data that outlives the function call.
What Are Lifetime Subtyping and Variance?
Lifetime subtyping ('a: 'b meaning 'a outlives 'b) lets you express that one lifetime is at least as long as another. It's used when working with multiple related references.
// 'long: 'short means 'long outlives 'short
fn longest_with_announcement<'long, 'short>(
x: &'long str,
y: &'long str,
ann: &'short str, // ann only needs to live during the call
) -> &'long str {
println!("Announcement: {}", ann);
if x.len() > y.len() { x } else { y }
}You rarely need explicit subtyping in application code, but it appears in library code and when writing generic data structures that hold multiple references with different constraints.
Variance is the more advanced concept that governs how lifetime relationships propagate through generic types. &'a T is covariant in 'a; you can use a longer-lived reference where a shorter-lived one is expected. &'a mut T is invariant in T; this prevents unsoundness when mutably borrowing. Understanding variance matters when writing generic data structures like Cell<T> or custom smart pointers, but application-level Rust code rarely requires explicit variance reasoning.
What Common Mistakes Do Rust Developers Make When Working with Lifetimes?
Lifetime errors have specific patterns that appear repeatedly. Here are the most common mistakes and the correct mental model to resolve them.
-
Trying to return a reference to a local variable. The single most common lifetime error for beginners. Local variables are dropped at the end of the enclosing scope. A reference to a local variable cannot escape that scope. The fix is almost always to return an owned value instead. If returning a reference is genuinely the right design, the referenced data must be owned by the caller and passed in as a parameter.
-
Using
'staticto silence lifetime errors without understanding why. When the compiler asks for a lifetime and you do not understand why, reaching for'staticis tempting. It works syntactically but is semantically incorrect: it promises that a reference lives forever when it may not. The correct fix is understanding which input the output reference comes from and annotating accordingly. Use'staticonly for data that genuinely lives in the binary (string literals, static variables,Box::leak). -
Expecting lifetime annotations to fix borrow checker errors they cannot solve. Lifetime annotations cannot make the borrow checker accept code that is genuinely unsound. If two mutable borrows overlap, no annotation makes that valid. If data is moved and then referenced, no annotation restores the moved value. When you cannot find a lifetime annotation that satisfies the compiler, the code structure itself needs to change: usually by using owned types,
Arc<Mutex<T>>, or restructuring the borrow to not overlap. -
Annotating struct lifetimes unnecessarily when ownership is the correct design. Structs with lifetime parameters are more complex to use than structs that own their data. A common mistake is designing a struct with borrowed fields (requiring lifetime parameters) when the struct should simply own the data. Unless your struct is specifically designed for zero-copy performance in a hot path, prefer
Stringover&str,Vec<T>over&[T], andPathBufover&Pathin struct fields. -
Confusing
&'a TandT: 'a.&'a Tis a reference toTthat lives for'a.T: 'ais a bound meaningTdoes not contain references shorter than'a. These are completely different constraints.T: 'staticdoes not meanTis a reference: it meansTcontains no non-static references. Owned types always satisfyT: 'staticbecause they own all their data. -
Not reading the compiler's suggestions carefully. The Rust compiler's lifetime error messages are among the best of any compiler. They typically name the specific lifetime causing the issue, show where the reference is created, and suggest what annotation is needed. Most lifetime errors are resolved by reading the error message carefully, not by guessing. The
--explain Exxxxflag provides detailed explanations of specific error codes that include examples of the problem and its solution.
Frequently Asked Questions
No. Lifetimes are compile-time only; they are completely erased before code generation. There is zero runtime cost. They exist purely so the compiler can verify reference validity statically. This is fundamentally different from garbage collection (runtime cost) and reference counting (runtime cost proportional to clone/drop frequency). Rust's lifetime system provides memory safety guarantees that GC provides at runtime, but at compile time. The safety check itself is free at runtime.
This usually means you are returning a reference from a function but the compiler cannot determine which input it comes from. Add 'a to both the parameter that is the source of the return value and to the return type to clarify the relationship. If the function does not take any reference parameters and tries to return a reference, the data being referenced must be 'static; or you need to return an owned type instead.
When you need to store a reference in a global, pass a reference to tokio::spawn, or guarantee that something lives for the program's duration. Do not reach for 'static just to silence lifetime errors; it is often a sign you should be using owned data instead. The three legitimate uses of 'static: string literals (&'static str), references to static variables, and data leaked into static memory with Box::leak.
They are related but distinct. Drop order is determined by scope (inner scopes drop first). Lifetimes are the compiler's way of verifying that references remain valid given the drop order. The borrow checker enforces that you never use a reference after its referent is dropped. You can think of lifetimes as "the compiler's model of drop order for references"; they encode which scopes references may safely span without the compiler needing to perform runtime checks.
&'a T is a reference to T that lives for at least 'a. T: 'a is a bound meaning the type T contains no references shorter than 'a; it does not mean T is a reference. Owned types like String and Vec<T> always satisfy T: 'static because they own all their data. The distinction matters when writing generic functions: fn foo<T: 'static>(x: T) accepts any owned type and any 'static reference, but fn foo<T>(x: &'static T) requires a 'static reference specifically.
Yes; macros that generate code with references can produce lifetime errors that are hard to diagnose because the error location points to macro expansion sites rather than the original source. If you encounter a lifetime error inside a derive macro or procedural macro expansion, check whether the type you are deriving on has reference fields. Derive macros like serde::Deserialize and Clone handle lifetime parameters automatically when derived correctly, but custom proc macros may require explicit handling.
Trait objects (&dyn Trait and Box<dyn Trait>) have implicit lifetime bounds. &dyn Trait is equivalent to &'_ dyn Trait + '_; both the reference and the trait object itself have a lifetime. Box<dyn Trait> is equivalent to Box<dyn Trait + 'static> by default; the trait object must contain no non-static references unless you explicitly bound it with Box<dyn Trait + 'a>. This is a common source of confusion when storing closures or trait objects in structs: Box<dyn Fn() + 'static> requires the closure to capture only owned or 'static data.
Related Glossary Terms
- Lifetime: Lifetime annotation syntax and elision rules explained
- Reference:
&Tand&mut T; the types that lifetimes govern - Borrow Checker: The compiler system that enforces lifetime rules
- Ownership: Lifetimes are an extension of Rust's ownership model
- Generic: Lifetime parameters are a form of generic parameter
- Trait: Trait bounds and lifetime bounds work together in function signatures
- dyn Trait: Trait objects have implicit lifetime bounds (
Box<dyn Trait + 'static>) - Struct: Structs holding references require explicit lifetime annotations
- Closure: Closures capture references; their lifetimes affect the closure's lifetime
Sources
- The Rust Book: Validating References with Lifetimes
- Rustonomicon: Lifetimes
- Rust Reference: Lifetime elision
- Jon Gjengset: Crust of Rust: Lifetime Annotations (YouTube)
- JetBrains Developer Ecosystem Survey 2025: Rust pain points
If you want a structured path through Rust's ownership system (lifetimes, borrowing, trait bounds, and the patterns that make them click), Rustify's 9-week bootcamp offers 1:1 coaching and a curriculum designed to take you from "fighting the borrow checker" to "understanding it well enough to use it as a tool."
