Lifetimes pick up where the ownership post left off. References don't outlive their referents, but how does the compiler actually prove it? Swift outsources the proof to ARC at runtime: the count keeps the value alive as long as anyone holds a strong reference, and weak nils out automatically when the referent is freed. Rust has no runtime to lean on. References are bare pointers underneath, no refcount, no nil-out, so the compiler has to check at compile time that every borrow ends before the value it borrows from. The mechanism is to give names, written 'a, to the regions of code over which each reference is valid, and to check that each borrow's region is contained within the region of the value it borrows from.
Most of the time those names stay implicit, inferred by the compiler under a small set of elision rules. The cases where you have to write them out are narrower than the syntax suggests.
What 'a actually labels
The first thing to put aside is the word "lifetime" itself. The compiler is not tracking anything at runtime. A lifetime is a static label for the region of source code over which a reference is valid. The name is inherited from research on region-based memory management; "region" or "scope label" would be clearer, but "lifetime" is the convention.
The simplest case the compiler proves is the one you write every day without noticing:
fn main() {
let s = String::from("hello");
let r = &s;
println!("{r}");
}
The borrow &s produces a reference whose region of validity starts where the borrow expression runs and ends where the reference is last used. The compiler checks that this region is contained within the lifetime of the value being borrowed (the local s, which lives until the end of main). Because the region is contained, the program is sound and the code compiles with no annotations.
The case the compiler rejects is the one where the inclusion fails:
fn main() {
let r;
{
let s = String::from("hello");
r = &s;
}
println!("{r}");
}
r's region extends past the inner block, but s is dropped at the closing brace. The compiler emits borrowed value does not live long enough, because the region for r is no longer contained within the region for s. The compile-time check matches one-to-one with the dynamic behaviour the runtime would otherwise have to police.
In Swift, the runtime does the policing. The same shape, using a class so reference counting applies:
class Buffer {
let data = "hello"
}
var r: Buffer
do {
let s = Buffer()
r = s
}
print(r.data)
By the time the inner block exits, s is gone but r still holds a strong reference, so ARC keeps the buffer alive. The runtime is the proof; the compiler has nothing to ask for. Rust's compiler asks for proof because there is no runtime to consult, and 'a is the syntax it uses to name the region a reference is valid for so it can carry that constraint through the type system.
Every reference, written or not, has an associated lifetime parameter the compiler picks for it. Annotated by hand, the simple example looks like this:
fn main() {
let s = String::from("hello"); // 's: from this line to the end of main
let r = &s; // 'r: from this line to its last use below
println!("{r}"); // 'r ends here
} // 's ends here
The compiler reasons over 's and 'r and checks one constraint: 'r is contained within 's. The borrow &s produces a reference of type &'r String, and 'r cannot extend past 's because that would mean reading from a freed value. In the broken second example, the constraint failed.
Inside a function body you cannot write 'r and 's yourself. The compiler picks the names and the regions, and there is no surface syntax for naming them at a binding site. Lifetime names become writable in two places: function signatures and struct definitions.
The type-system framing falls into place from there. Lifetimes are generic parameters, in the same family as type parameters. Vec<T> carries a type parameter T and at each call site the compiler picks a concrete type. &'a T carries a lifetime parameter 'a and at each call site the compiler picks a concrete region. The choice is constrained: the region must be contained within the lifetime of the value being borrowed, and it must cover every point where the reference is used. When several constraints apply, the compiler picks the smallest region that satisfies all of them. Two consequences follow. Lifetimes are erased before the program runs; the compiled binary has no lifetime information, only the pointers themselves. And the leading single quote in 'a is the conventional marker that distinguishes a lifetime parameter from a type parameter or any other identifier.
Elision: why most signatures don't carry lifetimes
If every reference has a lifetime parameter, you'd expect every function taking or returning a reference to carry parameters in its signature. In practice, almost no Rust code looks like that. The mechanism that closes the gap is elision: a small set of rules the compiler applies to fill in lifetime parameters the programmer would otherwise have to write.
Elision is purely a syntactic convenience. The compiler still tracks lifetimes; it just spares you naming them when the right answer is unambiguous. The rules apply only to function and method signatures, not to function bodies, and they fail conservatively. When the rules can't determine a unique answer, the compiler refuses the elided form and asks for explicit annotations.
There are three.
Rule 1: each input reference position gets its own lifetime parameter. Two reference parameters mean two distinct lifetime parameters in the desugared signature.
fn print_pair(a: &str, b: &str) {
println!("{a} {b}");
}
// Compiler-applied desugaring:
fn print_pair<'a, 'b>(a: &'a str, b: &'b str) {
println!("{a} {b}");
}
This rule alone covers any function that takes references but returns none. There's no output lifetime to assign, and the inputs being independent is harmless.
Rule 2: if there is exactly one input lifetime, that lifetime is assigned to every output lifetime. When a function has one reference parameter and returns a reference, the compiler assumes the returned reference borrows from that parameter.
fn first_word(s: &str) -> &str {
s.split(' ').next().unwrap()
}
// Compiler-applied desugaring:
fn first_word<'a>(s: &'a str) -> &'a str {
s.split(' ').next().unwrap()
}
Rule 2 is the workhorse. Most accessor-shaped functions take one reference and return something derived from it. The compiler picks the only consistent answer, and the signature stays clean.
Rule 3: if one of the input parameters is &self or &mut self, the lifetime of self is assigned to every output lifetime. This is what makes method signatures look natural.
struct Buffer { data: String }
impl Buffer {
fn name(&self) -> &str {
&self.data
}
}
// Compiler-applied desugaring:
impl Buffer {
fn name<'a>(&'a self) -> &'a str {
&self.data
}
}
Rule 3 covers the case rule 2 cannot. A method signature like fn find(&self, key: &str) -> &str has two input lifetimes, so rule 2 has no unique answer to give. Rule 3 picks &self's lifetime for the output, on the assumption that the returned reference borrows from the receiver, not from the search key.
The three rules apply in sequence. Rule 1 always runs and gives every input its own lifetime. Rule 2 fires when there is exactly one input lifetime and assigns it to all outputs. Rule 3 fires when &self or &mut self is among the inputs and assigns self's lifetime to all outputs. If after all three rules some output lifetime is still unassigned, the elided form fails and the compiler asks for explicit annotations.
The failure case is easy to provoke. Two non-self reference parameters and a reference return is not covered by any rule:
fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}
The compiler emits missing lifetime specifier and notes that it can't decide whether the output borrows from a or b. There is no default answer the language is willing to assume. The function author has to say which input the output comes from. The next section handles that case.
The Swift parallel here is mostly the absence of one. Swift has no lifetime parameters at all, so its function signatures look like elided Rust signatures everywhere: func firstWord(_ s: String) -> String has no extra machinery, and a class accessor returning a property reads as a property access with no annotations. Elision is what closes the gap between the two languages on day-to-day code. The lifetime story is still being checked, but it stays out of sight on every signature the elision rules cover, which is most of them.
Functions that need explicit annotations
longest is the canonical example because the compiler genuinely cannot decide what the elided signature should be. The function returns whichever of its two string slices is longer, so the return value's region of validity has to be tied to both inputs. Rule 2 cannot fire (two input lifetimes), rule 3 doesn't apply (no &self), and there is no fourth rule that picks a default. The author has to write the constraint down.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
The signature introduces a single lifetime parameter 'a, then ties both inputs and the output to it. The compiler reads this as a contract: at every call site, pick a region 'a such that both a and b are valid for at least 'a, and the returned reference will be valid for 'a too.
What the contract enforces in practice is that the output lives at most as long as the shorter of the two inputs. When the caller passes slices with different actual lifetimes, the compiler picks the largest region contained in both, and the returned reference is valid only for that intersection.
fn main() {
let a = String::from("hello");
let result;
{
let b = String::from("world!");
result = longest(&a, &b);
println!("{result}");
}
// println!("{result}"); // would not compile
}
Inside the inner block, both &a and &b are valid, the chosen 'a is the inner block's region, and the returned reference is usable inside it. The commented-out println! outside the block fails to compile, because 'a cannot extend past the lifetime of b, even though the returned reference may actually have come from a and would have been safe to use. The compiler is conservative: the contract says the output is valid for 'a, and 'a cannot exceed either input.
This is also where the difference between type parameters and lifetime parameters becomes concrete. A type parameter T resolves to a single concrete type the call site provides. A lifetime parameter 'a resolves to a single concrete region of code, picked to fit inside every constraint the call site implies. With one input, the choice is forced by that input alone. With two inputs sharing a lifetime, the choice is forced by their intersection.
Sometimes the dependency is asymmetric: the returned reference comes from one input, never the other. That's expressible by giving the inputs distinct lifetime parameters and naming only one in the output:
fn pick_first<'a, 'b>(a: &'a str, _b: &'b str) -> &'a str {
a
}
'b is unconstrained relative to 'a, so the caller can pass a short-lived b and still use the returned reference for as long as a is valid. The single-lifetime version of longest would have over-constrained this case by tying the output to whichever input lived shorter.
The Swift parallel is mostly that the case never comes up the same way. A Swift function picking the longer of two strings returns an owned String, because String is a value type and the function copies whichever input wins. The comparable case is a function returning a class instance derived from class arguments. Swift's runtime handles it through ARC, the returned object stays alive as long as the caller holds it, and there's no signature-level contract, no 'a, and no compile-time region check. Both designs prevent the same class of bug. Rust pays nothing at runtime and rejects some programs the Swift version would have run; Swift pays the refcount bump on assignment and accepts the program at compile time.
Structs that hold references
Function signatures are one place lifetime parameters become writable; struct definitions are the other. When a struct stores a reference rather than an owned value, the type system has no way to talk about the field's region unless the struct itself carries a lifetime parameter the field can borrow from. The syntax mirrors what generics already do for type parameters.
struct Parser<'a> {
input: &'a [u8],
cursor: usize,
}
Parser<'a> is a struct generic over a lifetime, exactly the way Vec<T> is a struct generic over a type. The field input: &'a [u8] says the parser holds a borrow whose region is 'a. At each construction site, the compiler picks a concrete 'a based on the slice the caller passes in, and the resulting parser is valid for at most that region.
The lifetime parameter has to flow through the impl block too, because methods working on the struct's borrowed field need to see the parameter:
impl<'a> Parser<'a> {
fn new(input: &'a [u8]) -> Self {
Parser { input, cursor: 0 }
}
fn peek(&self) -> Option<u8> {
self.input.get(self.cursor).copied()
}
}
The impl<'a> introduces the parameter, and Parser<'a> uses it to refer to the same struct. new mentions 'a explicitly because the caller's slice and the resulting parser have to share it. peek doesn't mention 'a directly: rule 3 from the previous section assigns &self's lifetime to the output, which works because peek returns an owned Option<u8> and no further lifetime is involved.
Construction looks ordinary. The compiler picks 'a from the slice the caller hands in:
fn main() {
let bytes = b"hello";
let parser = Parser::new(bytes);
println!("{:?}", parser.peek());
}
bytes is a slice into a string literal, valid for the whole function, so 'a resolves to that region. With an input whose lifetime is shorter, 'a would resolve to whichever region the slice was valid for, and the parser would be usable only inside it.
In Swift, the analogous shape is a class holding another class. There's no lifetime parameter because the strong reference does the same job at runtime, ARC keeps the buffer alive as long as the parser holds it, and the compiler has nothing to track. The closer parallel for "non-owning reference into something the holder doesn't own" is a weak reference, which doesn't retain and nils out automatically if the buffer is freed; the runtime is the safety net, every access has to unwrap an Optional, and the unwrap fails if the buffer went away. Rust's &'a [u8] field is the same idea expressed statically: the buffer cannot have been freed while the parser holds it, because the type system would have rejected any program that allowed it.
Storing references inside structs is harder to get right than storing owned values, and the borrow checker objects more often. The default in idiomatic Rust is to own the data unless borrowing saves something concrete: a real allocation, a copy of a large buffer, or a piece of input that has a clear longer-lived owner elsewhere. A Parser over a parse-and-discard buffer is a good fit for &'a [u8], because copying the input would defeat the point of streaming. A Person { name: &'a str } for a domain entity that has to outlive the file it was loaded from is usually the wrong shape: making it Person { name: String } drops the lifetime parameter and the constraint it imposes on every caller. Reach for borrowing fields when the alternative is a copy that does measurable work; reach for owning fields the rest of the time.
'static
There's one lifetime name Rust gives you out of the box: 'static. It is the lifetime that lasts for the entire run of the program, from start to exit. A reference of type &'static T is valid forever, in the sense that the value it points at is guaranteed to outlive any code that could ever read through that reference.
The most common source of 'static references is string literals. When you write "hello" in Rust source, the compiler bakes those bytes into the program's static data section, and the resulting expression has type &'static str. The bytes live for the full run of the program because they are part of the program.
fn greet() -> &'static str {
"hello"
}
This signature compiles even though the function has no input lifetimes for elision to work with: 'static is a concrete lifetime named directly by the author, not one the compiler had to infer. Any caller can hold the returned reference for as long as they want.
Two other sources produce 'static references. The first is static items:
static GREETING: &'static str = "hello, world";
GREETING has type &'static str because the compiler stores the string in the static data section and gives the binding a name pointing there. const items work similarly for inlined constant values, with the difference that const is a compile-time substitution rather than a single allocation.
The second is Box::leak, which takes ownership of a heap allocation and produces a reference that survives until the program exits, deliberately skipping the deallocation that would otherwise happen when the Box is dropped:
fn make_static_buffer(size: usize) -> &'static mut [u8] {
Box::leak(vec![0u8; size].into_boxed_slice())
}
Leaking sounds like a smell, but the use case is legitimate: configuration parsed at program start, large lookup tables initialised once, anything the program will hold for its full run. Once leaked, the value is never reclaimed, so the reference is 'static by construction. Use it sparingly; the memory is gone for the rest of the run.
'static also appears as a bound on type parameters, written T: 'static. The bound requires that values of type T be capable of living for the entire program: nothing inside the type is tied to a shorter region.
Types that own their data qualify automatically. An owned String, Vec<u8>, or Box<MyStruct> has no internal borrows, so a value can be held indefinitely without anything inside it expiring. Types that borrow only from 'static storage also qualify: a &'static str lasts forever by definition, so a struct holding only such references is fine. What fails the bound is any type containing a borrow with a shorter region, like a &'a str for some local 'a; the reference itself is only valid until 'a ends, so a value of that type cannot outlive the local scope.
The bound shows up wherever a value might outlive the stack frame that created it. std::thread::spawn requires its closure to satisfy T: 'static, because the spawned thread can keep running after the spawning function returns. Channel sends across threads carry the same requirement. The next section covers more of these.
Trait objects are one place this default shows up directly. Box<dyn Error> is shorthand for Box<dyn Error + 'static>; trait-object types carry an implicit 'static bound unless an explicit shorter lifetime is named. The errors post in this series used this default throughout, sometimes naming the bound explicitly (Error + Send + Sync + 'static) and sometimes leaving it implicit. Same mechanism, just different visibility.
The Swift parallels split across the two uses. For &'static T references, the closest counterpart is Swift's static storage. String literals are stored in the program's data section, and module-scope let constants live for the entire run. The difference is that Swift's type system never names this property; you simply use the constant and rely on the compiler to have placed it somewhere durable. ARC is bypassed entirely for static-storage data, because the data isn't heap-allocated and there's no refcount to maintain.
let greeting = "hello, world"
func greet() -> String {
return greeting
}
The string is part of the binary, the function returns a copy of it because String is a value type, and nobody has to think about lifetimes. The mechanism is identical at the runtime level; only the type-level visibility differs.
For the bound use of 'static, Swift has no direct equivalent. Class types can hold references to anything reachable through ARC, including objects with shorter conceptual lifetimes; the language has no way to express "this value must contain no shorter-lived references" because shorter-lived references in the Rust sense don't exist. Swift's nearest concept is the requirement for closures captured into long-running tasks to use [weak self] or [unowned self] to avoid retain cycles, which is a runtime correctness rule rather than a type-level constraint. Rust's T: 'static makes the same kind of constraint statically checkable.
Lifetime bounds on generics
T: 'static says "T can live for the entire program". The same syntax generalises. T: 'a says "T can live for at least the region 'a": any references inside T must outlive 'a. The bound shows up most often in struct definitions that combine a generic parameter with a borrowed reference.
struct Holder<'a, T: 'a> {
value: &'a T,
}
The bound says T's internal borrows, if any, all outlive 'a. This is required for soundness: a &'a T lets the holder read through the reference for the whole region 'a, and that read would be unsafe if T contained a shorter-lived borrow. In practice the compiler infers this bound in most struct definitions through implied bounds: any field of type &'a T already implies T: 'a, so you usually don't write the bound yourself. The explicit form matters in trait definitions and where clauses where the inference doesn't carry through.
The other shape is a bound between two lifetime parameters, 'b: 'a, read as "'b outlives 'a". It says any region named 'b is at least as long as the region named 'a.
struct Pair<'short, 'long: 'short> {
short: &'short str,
long: &'long str,
}
The bound lets the struct treat the long-lived reference as if it were short-lived when it wants to. A method that returns &'short str can return either self.short or self.long, because 'long: 'short means a 'long reference can be safely shortened to 'short. Without the bound, the compiler would refuse the second case.
impl<'short, 'long: 'short> Pair<'short, 'long> {
fn pick(&self, prefer_long: bool) -> &'short str {
if prefer_long { self.long } else { self.short }
}
}
The return type is &'short str. self.short already has that lifetime; self.long has lifetime 'long, but the bound allows the compiler to coerce it into &'short str at the return point. Without 'long: 'short, the second branch would fail to compile.
These bounds come up in library code more than in application code. Most application-level Rust uses owned values, the standard library, and a few &strs, none of which require explicit bounds. Library authors writing generic abstractions over borrowed data hit the bounds more often, because they have to spell out relationships between regions that application code can take for granted. Swift has no direct analogue: Swift generics carry type parameters but no lifetime parameters, and the underlying problem (a value containing references that must outlive a region) is handled at runtime by ARC.
What the annotations buy
It's worth pausing on what this whole apparatus replaces. ARC is the runtime version of the same proof: every strong-reference assignment compiles to a refcount increment, every reference going out of scope compiles to a decrement, and the value is freed when the count reaches zero. The increments are atomic when references cross thread boundaries, which makes them noticeably more expensive than ordinary memory writes. weak references add a side-table the runtime uses to nil out pointers when their referents are freed. Every class instance carries a small header with the count itself, typically a couple of machine words. None of this work is visible in source, but all of it executes on every assignment, return, and parameter pass.
Lifetime annotations replace that machinery entirely. There are no refcounts on &T or &mut T, no atomic operations, no header words, no nil-out side tables. References are bare pointers; assignment compiles to a register move. The compiler has already proved, before the program runs, that no reference outlives its referent, so the runtime has nothing left to check. The cost has moved from runtime to compile time, and from instructions to annotations. The annotations are not free. They take space in signatures, especially in library code, and they reject some programs that would have run correctly under ARC. The Rust answers exist (Rc<RefCell<T>>, restructuring, ownership transfer, occasionally unsafe), but the type-system version is sometimes more work to express than the runtime-checked version.
The honest framing of the trade-off: ARC pays per-operation runtime cost and accepts every well-typed program at compile time. Lifetimes pay zero runtime cost but reject some programs and ask the author to redesign them. Compile-time certainty is the larger win once you have it: a program that builds is provably free of use-after-free and dangling references, with the bug class eliminated at the binary level rather than caught at runtime. ARC's strength is ergonomic; it removes the friction of writing annotations, at the cost of leaving more for the developer to debug; most prominently the retain cycle, a bug class lifetimes don't have because there is nothing to retain when references are non-owning by default.
The next post is concurrency, where the borrow rules from the ownership post combine with the lifetime annotations from this one to give Rust a story for sharing data across threads: Send, Sync, Arc<Mutex<T>>, channels. Swift solves the same problem at runtime through Grand Central Dispatch and lock-based primitives like os_unfair_lock, with the runtime-versus-type-system contrast carrying through one more time.