env.dev

Constants in Rust: const, static, const fn & LazyLock

Rust splits constants two ways: const inlines at every use site, static has a fixed memory address. const fn computes at build time. LazyLock (Rust 1.80) gives thread-safe globals.

Last updated:

Rust splits "constant" into two distinct keywords with very different runtime behaviour. const declares a value the compiler evaluates and inlines at every use site — there is no fixed memory address, no global storage. static declares a single, fixed-address value that lives for the program's lifetime; you can take a reference to it. const fn bridges the two by letting you compute compile-time constants with real functions, including loops and pattern matching since Rust 1.46 (2020). And LazyLock, stabilized in Rust 1.80 (July 2024), finally gave the standard library a thread-safe, lazily-initialized global without external crates. This guide covers when to reach for which.

When should you use const vs static?

Use const when the value is a compile-time literal that the compiler can copy at every reference — array sizes, match patterns, generic parameters, magic-byte signatures. Use static when you need a single stable address (so a reference can outlive the function that produced it) or when the value is too large to inline cheaply.

rust
// const — inlined at every use site, no memory address
const MAX_BUFFER_SIZE: usize = 64 * 1024;          // 64 KiB
const DEFAULT_PORT: u16 = 8080;
const PI: f64 = std::f64::consts::PI;
const MAGIC_BYTES: [u8; 4] = [0x7F, b'E', b'L', b'F']; // ELF header

// Used in array sizes — only const can do this
let buffer = [0u8; MAX_BUFFER_SIZE];

// static — one fixed address, can be borrowed
static GLOBAL_CONFIG: &str = "production";
static VERSION: &str = env!("CARGO_PKG_VERSION");

fn current_version() -> &'static str { VERSION }

Both require an explicit type annotation; both must be initialized with a constant expression. Neither has a runtime "construct it now" phase — the value exists from program start.

Why is "static mut" almost always wrong?

A static mut is a global mutable variable. Every read and every write requires unsafe because the compiler cannot prove freedom from data races. The Rust 2024 edition went further and made static mut references a hard error by default — see RFC 3098 / "static_mut_refs" lint.

For thread-safe global state, use the standard library's LazyLock (Rust 1.80, 2024) or OnceLock, or atomics for primitives:

rust
use std::sync::{LazyLock, atomic::AtomicU32};

// Lazily initialized, thread-safe — runs the closure exactly once
static SETTINGS: LazyLock<Settings> = LazyLock::new(|| Settings::from_env());

// Thread-safe counter — no unsafe required
static COUNTER: AtomicU32 = AtomicU32::new(0);

fn increment() -> u32 {
    COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}

What can const fn do today?

Functions marked const fn can be called in constant contexts — their return value can initialize a const or sit as an array length. The feature has expanded edition by edition: control flow (1.46), references and pattern matching (1.46+), trait bounds (1.61), and more. As of recent stable, you get loops, branches, slice indexing, and calls to other const functions. Heap allocation and trait objects remain off the table.

rust
const fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

const FIB_20: u64 = fibonacci(20); // 6765 — computed at compile time

const fn kilobytes(kb: usize) -> usize { kb * 1024 }
const fn megabytes(mb: usize) -> usize { kilobytes(mb * 1024) }

const MAX_UPLOAD: usize = megabytes(50); // 52_428_800 — folded into the binary

Prefer const fn over macros for compile-time computation. It is type-safe, debuggable, and avoids the worst macro hygiene issues.

How do enums replace constants?

Rust enums are sum types — they can carry data per-variant — but for plain enumerations they serve the same role as enum constants in other languages. Combine a derived Copyimpl with associated const fn methods and exhaustive match, and you get type-safe, inline-able tag values:

rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
    Patch,
}

impl HttpMethod {
    const fn as_str(&self) -> &'static str {
        match self {
            Self::Get    => "GET",
            Self::Post   => "POST",
            Self::Put    => "PUT",
            Self::Delete => "DELETE",
            Self::Patch  => "PATCH",
        }
    }
}

// Compiler enforces exhaustive matching
fn handle(method: HttpMethod) {
    match method {
        HttpMethod::Get    => println!("Reading"),
        HttpMethod::Post   => println!("Creating"),
        HttpMethod::Put    => println!("Updating"),
        HttpMethod::Delete => println!("Deleting"),
        HttpMethod::Patch  => println!("Patching"),
    }
}

Mark public enums you may extend later with #[non_exhaustive] so downstream crates cannot rely on exhaustiveness — adding a variant becomes a non-breaking change.

Which standard-library constants matter?

The numeric primitives ship constants directly on the type, and std::f64::consts / std::f32::consts hold the mathematical ones:

rust
use std::f64::consts;

consts::PI         // 3.141592653589793
consts::E          // 2.718281828459045
consts::TAU        // 6.283185307179586
consts::SQRT_2     // 1.4142135623730951
consts::LN_2       // 0.6931471805599453

// Integer limits — on the type itself, no module needed
i32::MAX           // 2_147_483_647
i32::MIN           // -2_147_483_648
u64::MAX           // 18_446_744_073_709_551_615
usize::BITS        // 64 (on 64-bit platforms)

// Float limits
f64::INFINITY      // Positive infinity
f64::NEG_INFINITY  // Negative infinity
f64::NAN           // Use f64::is_nan() — never == NAN
f64::EPSILON       // ~2.2e-16
f64::MAX           // ~1.8e308

Practical patterns and gotchas

  • Naming: SCREAMING_SNAKE_CASE for both const and static. Associated constants on types follow the same rule.
  • Trait constants: trait Limit { const MAX: usize; } lets generic code define per-type constants — useful for crate-level numeric bounds.
  • env! reads at compile time; environment variables read at runtime use std::env::var. They are different mechanisms — see environment variables in Rust.
  • Don't fight the type system on global state: if you find yourself reaching for static mut, you almost certainly want LazyLock, an atomic, or a parameter passed through the call graph.

For language-agnostic naming, immutability, and "magic number" rules across languages, see constants best practices.

Was this helpful?

Read next

Constants in Python: typing.Final, Enum & SCREAMING_SNAKE

Python has no const keyword — constants are SCREAMING_SNAKE_CASE by PEP 8 convention. typing.Final (Python 3.8) adds static-checker enforcement; the enum module handles closed sets.

Continue →

Frequently Asked Questions

What is the difference between const and static in Rust?

const has no memory address — the compiler inlines the value at every use site. static occupies a single fixed address you can take a reference to. Use const for compile-time literals that participate in array sizes or match patterns; use static when you need a stable reference or for large values.

Why is static mut almost always wrong?

Every read and write requires unsafe because the compiler cannot prove freedom from data races. The Rust 2024 edition made static mut references a hard error by default (lint static_mut_refs). Use std::sync::LazyLock (stabilized in Rust 1.80, July 2024), OnceLock, or atomic types for thread-safe globals.

What can const fn do today?

Loops, branches, slice indexing, pattern matching, references, and calls to other const functions. Heap allocation and trait objects remain off the table. Use const fn over macros for compile-time computation — it is type-safe and debuggable.

How is env! different from std::env::var?

env! is a macro that reads at compile time and bakes the value into the binary. std::env::var reads at runtime from the process environment. They are different mechanisms — see the Rust env-vars guide.

Stay up to date

Get notified about new guides, tools, and cheatsheets.