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]