RustConstants & Statics

Constants & Statics in Rust

Rust provides two ways to declare values that live for the entire duration of a program: const and static. Both are global in nature, both require an explicit type, and both follow SCREAMING_SNAKE_CASE naming — but they work very differently under the hood. Understanding which to use, and when, prevents subtle bugs and leads to cleaner APIs.

const — Compile-Time Constants

A const declaration is evaluated entirely at compile time. The compiler replaces every use of the constant with its literal value — a process called inlining. Constants have no fixed memory address; they are more like named compile-time substitutions than real variables.

RUST
const MAX_CONNECTIONS: u32 = 100;
const PI: f64 = 3.141_592_653_589_793;
const APP_NAME: &str = "LetCodes";
const BUFFER_SIZE: usize = 1024 * 64; // 64 KB — arithmetic is allowed

fn main() {
    println!("App: {}", APP_NAME);
    println!("Max connections: {}", MAX_CONNECTIONS);
    println!("Buffer: {} bytes", BUFFER_SIZE);
    println!("Pi: {:.5}", PI);
}
App: LetCodes
Max connections: 100
Buffer: 65536 bytes
Pi: 3.14159
  • The type annotation is required — Rust never infers the type of a const

  • The value must be a constant expression — computed entirely at compile time

  • Can be declared at any scope, including the global (module) level

  • Naming convention is SCREAMING_SNAKE_CASE

  • The value is inlined at every use site — there is no single memory address

  • const can never be mut

static — Global Variables with a Fixed Address

A static declaration creates a global variable that lives for the 'static lifetime (the entire program run). Unlike const, a static has a single, fixed memory address. Every place that refers to the static accesses the same memory location.

RUST
static GREETING: &str = "Hello, world!";
static MAX_RETRIES: u32 = 3;
static NEWLINE: char = '
';

fn print_with_greeting(name: &str) {
    println!("{} I am {}.", GREETING, name);
}

fn main() {
    print_with_greeting("Rust");
    println!("Will retry up to {} times", MAX_RETRIES);
}
Hello, world! I am Rust.
Will retry up to 3 times
When does the address matter?
The fixed address of static matters when you need a stable pointer — for example, when working with embedded hardware registers, FFI callbacks that store a pointer, or situations where two parts of the program must agree they are looking at the same memory location.
const vs static vs let — Comparison

Feature

const

static

let

Keyword

const

static

let

Type annotation

Required

Required

Optional (inferred)

Evaluated at

Compile time

Compile time

Runtime

Memory address

None — inlined

Fixed single address

Stack (per call)

Lifetime

N/A (inlined)

'static (entire program)

Enclosing scope

Can be mut

Never

Yes (unsafe)

Yes (let mut)

Allowed in global scope

Yes

Yes

No

Naming convention

SCREAMING_SNAKE_CASE

SCREAMING_SNAKE_CASE

snake_case

static mut — Global Mutable State

Adding mut to a static creates a globally mutable variable. Both reading from and writing to a static mut require an unsafe block — Rust forces you to acknowledge that you are taking responsibility for preventing data races yourself.

RUST
static mut COUNTER: u32 = 0;

fn increment() {
    // Reading or writing static mut is always unsafe
    unsafe {
        COUNTER += 1;
    }
}

fn get_count() -> u32 {
    unsafe { COUNTER }
}

fn main() {
    increment();
    increment();
    increment();
    println!("Count: {}", get_count());
}
Count: 3
static mut is dangerous in multi-threaded code
Global mutable state via static mut is not thread-safe. If two threads call increment() simultaneously, the result is undefined behaviour. Prefer std::sync::Mutex, std::sync::atomic, or std::sync::RwLock for shared mutable state across threads.
Safe Alternatives to static mut

Rust's standard library provides thread-safe types that replace nearly every use case for static mut. These are idiomatic and safe:

RUST
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex;

// Atomic integer — safe to share across threads without locks
static REQUEST_COUNT: AtomicU32 = AtomicU32::new(0);

// Mutex-protected value — works for any type
static CONFIG: Mutex<&str> = Mutex::new("default");

fn record_request() {
    REQUEST_COUNT.fetch_add(1, Ordering::Relaxed);
}

fn update_config(new_cfg: &'static str) {
    let mut cfg = CONFIG.lock().unwrap();
    *cfg = new_cfg;
}

fn main() {
    record_request();
    record_request();
    update_config("production");

    println!("Requests: {}", REQUEST_COUNT.load(Ordering::Relaxed));
    println!("Config:   {}", CONFIG.lock().unwrap());
}
Requests: 2
Config:   production
const fn — Compile-Time Functions

A const fn is a function that can be called at compile time to produce a const value. This allows you to compute constants using real function logic rather than raw literals, making code more readable and less error-prone.

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

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

const RECV_BUFFER: usize = kilobytes(8);   // 8 192 bytes
const SEND_BUFFER: usize = kilobytes(16);  // 16 384 bytes
const CACHE_SIZE:  usize = megabytes(2);   // 2 097 152 bytes

fn main() {
    println!("recv buffer : {} bytes", RECV_BUFFER);
    println!("send buffer : {} bytes", SEND_BUFFER);
    println!("cache size  : {} bytes", CACHE_SIZE);
}
recv buffer : 8192 bytes
send buffer : 16384 bytes
cache size  : 2097152 bytes
const fn evolves with each Rust edition
The set of operations allowed inside const fn grows with each Rust release. As of Rust 1.79, you can use loops, if/else, pattern matching, and most arithmetic inside const fn. Check the release notes if a feature you want is not yet stable.
Practical Examples

Here are common real-world patterns where const and static shine.

HTTP status codes as constants

RUST
// Constants for well-known values that never change
const HTTP_OK:                u16 = 200;
const HTTP_NOT_FOUND:         u16 = 404;
const HTTP_INTERNAL_ERROR:    u16 = 500;

const MAX_HEADER_SIZE:        usize = 8 * 1024;   // 8 KB
const DEFAULT_TIMEOUT_SECS:   u64   = 30;

// A static string is zero-cost — it lives in the binary's read-only section
static API_BASE_URL: &str = "https://api.example.com/v1";
static USER_AGENT:   &str = "LetCodes/1.0";

fn describe_status(code: u16) -> &'static str {
    match code {
        200 => "OK",
        404 => "Not Found",
        500 => "Internal Server Error",
        _   => "Unknown",
    }
}

fn main() {
    println!("Base URL  : {}", API_BASE_URL);
    println!("User-Agent: {}", USER_AGENT);
    println!("Timeout   : {}s", DEFAULT_TIMEOUT_SECS);
    println!("Max header: {} bytes", MAX_HEADER_SIZE);
    println!("{} -> {}", HTTP_OK, describe_status(HTTP_OK));
    println!("{} -> {}", HTTP_NOT_FOUND, describe_status(HTTP_NOT_FOUND));
}
Base URL  : https://api.example.com/v1
User-Agent: LetCodes/1.0
Timeout   : 30s
Max header: 8192 bytes
200 -> OK
404 -> Not Found
When to Use const vs static vs let
  1. Use const for values that are truly fixed and meaningful to the program logic — mathematical constants, size limits, magic numbers. Prefer const over static for simple scalar values.

  2. Use static when you need a stable memory address, when the value is a string literal shared widely, or when interoperating with C via FFI.

  3. Use static + Mutex/Atomic when you need shared mutable state that is safe across threads.

  4. Use let for everything else — ordinary local variables computed at runtime.

  5. Avoid static mut unless you are writing low-level embedded code or FFI glue; always prefer the safe atomic/mutex wrappers.

Success
Constants in Rust are inlined by the compiler with zero runtime overhead, making them a strictly better choice than magic numbers scattered through your code. Name your constants well and you get self-documenting, refactor-friendly code that is just as fast as raw literals.