Lifetimes

Static References vs. Static Variables

In Rust, the keyword static appears in three distinct constructs that are easy to confuse: a static variable (static NAME: T = ...), a 'static reference (&'static T), and a 'static trait bound (T: 'static).

Side-by-Side Comparison

Construct Syntax Where It Lives in Memory Created At Dropped (Drop::drop)?
static variable static COUNT: u32 = 10; Fixed address in binary data/BSS segment Compile time Never dropped
&'static T reference let s: &'static str = "hi"; Pointer on stack; target in .rodata, static, or leaked heap Compile time or runtime (Box::leak) Pointer is Copy; target is not dropped
const item const MAX: u32 = 10; Inlined at every use site (no fixed address required) Compile time Re-materialized at each use site
T: 'static bound fn spawn<T: 'static>(v: T) Stack or heap (normal owned value) Runtime or compile time Dropped normally when owner goes out of scope

static Variables (static NAME: T)

A static item represents a single global memory location with a fixed address for the entire execution of the program. Because multiple threads can read a static variable simultaneously, the type T of a shared static variable must implement the Sync trait:

use std::sync::atomic::{AtomicUsize, Ordering};

// Lives at one fixed memory address for the entire program:
static REQUEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
static APP_NAME: &str = "RustServer";

fn record_request() -> usize {
    REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed) + 1
}

fn main() {
    println!("App: {APP_NAME} (address: {:p})", &APP_NAME);
    println!("Request #{}", record_request());
    println!("Request #{}", record_request());
}
App: RustServer (address: 0x10299c000)
Request #1
Request #2

Runtime-Initialized Statics (std::sync::LazyLock)

When a static variable requires runtime initialization (such as building a HashMap or reading an environment variable once), the standard library provides std::sync::LazyLock<T> and std::sync::OnceLock<T>:

use std::collections::HashMap;
use std::sync::LazyLock;

static STATUS_TEXT: LazyLock<HashMap<u16, &'static str>> = LazyLock::new(|| {
    let mut map = HashMap::new();
    map.insert(200, "OK");
    map.insert(404, "Not Found");
    map.insert(500, "Internal Server Error");
    map
});

fn main() {
    // Accessing `&*STATUS_TEXT` yields a `&'static HashMap<u16, &'static str>`:
    let status_ref: &'static HashMap<u16, &'static str> = &STATUS_TEXT;
    println!("200 -> {:?}", status_ref.get(&200));
    println!("404 -> {:?}", status_ref.get(&404));
}
200 -> Some("OK")
404 -> Some("Not Found")

&'static T Without a static Variable

Every &STATIC_VAR has type &'static T, but not every &'static T comes from a static variable. You can create a &'static T at runtime by leaking a heap allocation with Box::leak, or at compile time via constant promotion:

fn make_static_slice(n: u32) -> &'static [u32] {
    let v = vec![n, n * 2, n * 3];
    // Leaks the heap buffer intentionally and returns `&'static mut [u32]`,
    // which coerces to `&'static [u32]`:
    Vec::leak(v)
}

fn main() {
    let dynamic_static: &'static [u32] = make_static_slice(7);
    println!("Runtime-created &'static [u32]: {dynamic_static:?}");
}
Runtime-created &'static [u32]: [7, 14, 21]