TL;DR:
OnceLock<T>andLazyLock<T>are the standard library's answer to lazy static initialization.OnceLockis a cell that can be written exactly once and read many times; thread-safe, no dependencies.LazyLockwrapsOnceLockwith an initialization closure, making it a direct replacement for the popularlazy_static!macro. Both are stable since Rust 1.70 (OnceLock) and 1.80 (LazyLock). Theonce_cellcrate predates them and adds non-thread-safe variants (OnceCell) and more ergonomics.
What Is OnceLock<T>?
OnceLock<T> is a thread-safe cell that accepts one write and allows unlimited reads after that.
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
fn get_config() -> &'static str {
CONFIG.get_or_init(|| {
std::env::var("APP_CONFIG").unwrap_or_else(|_| "default".to_string())
})
}get_or_init initializes on first call and returns a reference on all subsequent calls. The closure only runs once, even under concurrent access.
What Is LazyLock<T>?
LazyLock<T> is a OnceLock with the initialization closure baked in; the closest equivalent to lazy_static!.
use std::sync::LazyLock;
use std::collections::HashMap;
static LOOKUP: LazyLock<HashMap<&str, u32>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert("one", 1);
m.insert("two", 2);
HashMap::from([("one", 1), ("two", 2)])
});
fn main() {
println!("{:?}", LOOKUP.get("one")); // Some(1)
}*LOOKUP triggers initialization on first dereference and caches the value for the lifetime of the program.
How Does once_cell Differ From std?
once_cell provides both thread-safe (sync::OnceCell) and single-threaded (unsync::OnceCell) variants, plus Lazy wrappers.
[dependencies]
once_cell = "1"use once_cell::sync::Lazy;
static REGEX: Lazy<regex::Regex> = Lazy::new(|| {
regex::Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap()
});| Type | Thread-safe | Init closure | From |
|---|---|---|---|
std::sync::OnceLock | ✅ | No (.get_or_init()) | std 1.70 |
std::sync::LazyLock | ✅ | Yes | std 1.80 |
once_cell::sync::OnceCell | ✅ | No | crate |
once_cell::sync::Lazy | ✅ | Yes | crate |
once_cell::unsync::OnceCell | ❌ | No | crate |
For new code targeting Rust ≥ 1.80, prefer std::sync::LazyLock. For older MSRV or non-thread-safe use, use once_cell.
Why Not Just Use lazy_static!?
lazy_static! was the ecosystem standard before std added these types. Prefer LazyLock for new code.
lazy_static! uses a macro to create a struct that wraps OnceLock internally; LazyLock is essentially the same thing but ergonomic and in std. No dependencies required.
Frequently Asked Questions
OnceLock and LazyLock leave the cell uninitialized if the closure panics. The next call will retry initialization. once_cell has the same behavior.
No. Write once is by design. If you need a resettable cell, use Mutex<Option<T>>.
After initialization, OnceLock reads are a single atomic load; significantly faster than a mutex lock. Use OnceLock when you write once and read often.
Sources
Related Glossary Terms
- mutex: For mutable shared state that changes repeatedly
- const-static: For truly compile-time constants
- send-sync: Why
OnceLockis thread-safe
Keep Reading
- Learn Rust in 2026: Global lazy state is a common pattern every Rust developer encounters

