OnceLock & LazyLock in Rust: vs lazy_static! Guide

Max WellsMax WellsFounder of Rustify

TL;DR: OnceLock<T> and LazyLock<T> are the standard library's answer to lazy static initialization. OnceLock is a cell that can be written exactly once and read many times; thread-safe, no dependencies. LazyLock wraps OnceLock with an initialization closure, making it a direct replacement for the popular lazy_static! macro. Both are stable since Rust 1.70 (OnceLock) and 1.80 (LazyLock). The once_cell crate 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()
});
TypeThread-safeInit closureFrom
std::sync::OnceLockNo (.get_or_init())std 1.70
std::sync::LazyLockYesstd 1.80
once_cell::sync::OnceCellNocrate
once_cell::sync::LazyYescrate
once_cell::unsync::OnceCellNocrate

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


  • mutex: For mutable shared state that changes repeatedly
  • const-static: For truly compile-time constants
  • send-sync: Why OnceLock is thread-safe

Keep Reading

  • Learn Rust in 2026: Global lazy state is a common pattern every Rust developer encounters

Ready to Land a $80-120k Rust Job?