How to Use Constants and Static Variables in Rust

Use `const` for compile-time constants that are inlined everywhere they are used, and `static` for variables that occupy a single, fixed memory location with a `'static` lifetime.

Use const for compile-time constants that are inlined everywhere they are used, and static for variables that occupy a single, fixed memory location with a 'static lifetime. Choose const for values known at compile time like configuration flags or mathematical constants, and static only when you need a mutable global state (wrapped in Mutex or Atomic) or a read-only global that must have a unique address.

Here is how you define and use them:

// A compile-time constant. It is copied into every place it is used.
const MAX_USERS: u32 = 100;

// A read-only static variable. It has a single memory address.
static API_KEY: &str = "secret-key-123";

// A mutable static requires synchronization primitives to be safe.
use std::sync::Mutex;
static COUNTER: Mutex<u32> = Mutex::new(0);

fn main() {
    // Constants are inlined; no memory address to take.
    let limit = MAX_USERS; 

    // Statics can have their address taken, though usually unnecessary.
    let key_addr = std::ptr::addr_of!(API_KEY);

    // Accessing mutable statics requires locking.
    let mut count = COUNTER.lock().unwrap();
    *count += 1;
}

The critical distinction is memory layout and safety. const values are duplicated in the binary wherever they are referenced, meaning they do not have a unique memory address. This makes them ideal for performance and ensures they are immutable by definition. You cannot take a reference to a const directly in a way that implies a unique location.

static variables, conversely, exist at a specific memory address for the entire duration of the program. While static can be read-only (like API_KEY above), it is most commonly used for mutable global state. Because Rust's borrow checker cannot guarantee thread safety for raw mutable globals, you must wrap mutable static variables in synchronization types like Mutex, RwLock, or atomic types (AtomicUsize, etc.). Attempting to mutate a static directly without these wrappers will result in a compile-time error unless you use unsafe blocks, which you should avoid unless absolutely necessary.

A common pitfall is trying to initialize a static with a value that isn't known at compile time. const and static (without lazy_static or once_cell) must be initialized with constant expressions. If you need lazy initialization or complex runtime setup, prefer the lazy_static crate or the standard library's std::sync::OnceLock (available in Rust 1.70+) instead of a raw static.

// Using OnceLock for lazy initialization (Rust 1.70+)
use std::sync::OnceLock;

static CONFIG: OnceLock<String> = OnceLock::new();

fn init_config() {
    CONFIG.set("runtime-config".to_string()).unwrap();
}

In summary, default to const for almost all global values. Only reach for static when you explicitly need a shared memory address, typically for mutable global state protected by concurrency primitives.