Using Rust's `once_cell` for Lazy Initialization in High-Performance Services
The Problem: Expensive Setup on Every Request
In a recent service I worked on, each HTTP handler needed to compile a large regular expression before it could validate incoming payloads. The regex was built from a static list of patterns that never changed at runtime. Compiling it on every request added measurable latency, especially under load, and wasted CPU cycles that could have been spent on actual business logic.
Moving the compilation to a static variable seemed obvious, but Rust’s rules around mutability and initialization order made a naive static REGEX: Regex impossible without unsafe code or a lazy‑initialization crate. I wanted a solution that was zero‑cost after the first use, thread‑safe, and idiomatic.
The Solution: once_cell::sync::Lazy
The once_cell crate provides a Lazy type that runs a closure the first time the value is accessed and then caches the result. The sync variant adds the necessary Send and Sync bounds, making it safe to share across threads—a perfect fit for server‑side workloads.
Why does this work well? The closure is executed exactly once, even when multiple threads race to initialize the value. Internally, Lazy uses an atomic state and a parking lot‑style wait mechanism, so there is no lock contention after initialization. The API is simple: you define a static Lazy and dereference it like a normal reference.
Real‑World Example: Lazy Regex Compilation
use once_cell::sync::Lazy;
use regex::Regex;
/// A static lazy‑initialized regex that matches allowed usernames.
/// The pattern is compiled once, on first use, and then reused.
static USERNAME_REGEX: Lazy = Lazy::new(|| {
// This closure runs only once.
Regex::new(r"^[a-zA-Z0-9_]{3,20}$").expect("Invalid username regex")
});
fn validate_username(input: &str) -> bool {
// Deref the Lazy to get a &Regex; no extra cost after init.
USERNAME_REGEX.is_match(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_and_invalid() {
assert!(validate_username("alice_123"));
assert!(!validate_username("ab")); // too short
assert!(!validate_username("invalid!char"));
}
}
Notice how the closure captures no external state; if you needed configuration, you could pass it via a function that returns a Lazy or use the once_cell::unsync::Lazy variant for single‑threaded contexts.
Why Not lazy_static?
You might be familiar with the lazy_static macro, which serves a similar purpose. In my experience, once_cell offers a few advantages:
- It separates the type (
Lazy) from the initialization logic, making the code easier to read and reason about. - The crate is actively maintained with a focus on minimal dependencies and compile‑time impact.
- For async contexts,
once_cellprovidesOnceCellprimitives that can be combined withasync_lockortokio::sync::OnceCellwhen you need non‑blocking initialization.
That said, if you already depend on lazy_static and are happy with it, there’s no urgent need to migrate. The choice often comes down to personal taste and the specific guarantees you need.
Pitfalls to Keep in Mind
Even though Lazy is easy to use, there are a couple of subtle points:
- If the initialization closure panics, the
Lazyenters a poisoned state and subsequent accesses will also panic. Make sure your closure is infallible or wrap it inResultand handle errors appropriately. - Avoid capturing large temporary data that lives only for the initialization; otherwise you might inadvertently keep it alive for the entire program lifetime.
- Remember that
Lazygives you a dereference to the inner type, not the type itself. If you need to own the value (e.g., to move it into another thread), you must clone or useLazy::forceto get a reference and then clone the inner value if it’sClone.
When to Reach for This Pattern
Any time you have data that is:
- expensive to compute,
- immutable after creation, and
- needed from multiple threads or async tasks,
consider a lazy static. Examples include:
- Compiled regexes or parsers,
- Configuration objects loaded from files or environment variables,
- Database connection pools (though dedicated pool crates exist),
- Pre‑computed lookup tables.
In my day‑to‑day work, I keep a small snippet like the one above in a shared utilities crate. It saves me from repeating boilerplate and makes the intent clear to anyone reading the code: “this value is prepared once and then reused forever.”
Wrap‑Up
Lazy initialization with once_cell::sync::Lazy is a lightweight, zero‑abstraction way to achieve thread‑safe, on‑demand setup in Rust. It removes runtime overhead after the first hit, keeps your globals safe, and integrates smoothly with the rest of the ecosystem. Give it a try the next time you notice a costly static initialization creeping into your hot paths—you’ll likely see both cleaner code and better performance.