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.
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
constThe 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_CASEThe value is inlined at every use site — there is no single memory address
constcan never bemut
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.
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
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.
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 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:
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.
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 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
// 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
Use
constfor values that are truly fixed and meaningful to the program logic — mathematical constants, size limits, magic numbers. Preferconstoverstaticfor simple scalar values.Use
staticwhen you need a stable memory address, when the value is a string literal shared widely, or when interoperating with C via FFI.Use
static+Mutex/Atomicwhen you need shared mutable state that is safe across threads.Use
letfor everything else — ordinary local variables computed at runtime.Avoid
static mutunless you are writing low-level embedded code or FFI glue; always prefer the safe atomic/mutex wrappers.