When the standard library isn't enough
You're writing a number guessing game. The user types a guess, and your code needs to pick a secret value between 1 and 100. In Python, you grab random.randint. In JavaScript, you call Math.random(). Rust doesn't include randomness in the standard library. The standard library focuses on what every program needs, and randomness is a specialized tool with many trade-offs. You reach for the rand crate.
The rand crate is the community standard for random number generation. It handles the messy details of entropy, seeding, and algorithms so you can focus on your logic. It provides fast generators, statistical distributions, and tools for reproducible testing.
How randomness actually works
Computers are deterministic machines. They calculate the next number based on the previous one. True randomness requires physical chaos. rand bridges the gap between deterministic code and chaotic reality using a seeded pseudo-random number generator.
Think of the operating system as a chaotic room. People drop dice, spin coins, and measure disk latency. This mess of unpredictable data is the entropy pool. rand reaches into that pool once to grab a handful of chaos. It uses that chaos to seed a local dice roller. Once seeded, the dice roller generates numbers incredibly fast without bothering the OS.
thread_rng() gives you a handle to that local dice roller for the current thread. The first call seeds the roller from the OS entropy pool. Subsequent calls return the same seeded instance. This design balances security and performance. You get cryptographic-quality seeding without the overhead of asking the OS for entropy on every single number.
Minimal example
Add rand to your dependencies and import the Rng trait. The trait brings methods like gen_range into scope.
[dependencies]
# Convention: omit the patch version.
# Cargo will resolve to the latest compatible 0.8.x release.
rand = "0.8"
use rand::Rng;
/// Generates a secret number between 1 and 100 inclusive.
fn main() {
// thread_rng() returns a thread-local RNG instance.
// It seeds automatically on first use via OS entropy.
let mut rng = rand::thread_rng();
// gen_range creates a random value within the range.
// The range syntax 1..=100 includes both endpoints.
let secret = rng.gen_range(1..=100);
println!("Secret: {secret}");
}
Run this code twice. The number changes. That's the point.
Walkthrough: traits, ranges, and lazy seeding
The Rng trait is the core interface. It defines methods like gen_range, gen_bool, and gen. You must import use rand::Rng; to access these methods. If you forget the import, the compiler rejects you with E0599 (no method named gen_range found for struct ThreadRng). The trait is the bridge between the RNG instance and the generation methods.
gen_range accepts any type that implements the Distribution trait. The Distribution trait tells the RNG how to produce values of a specific type. Ranges like 1..=100 implement Distribution<u32>. Slices implement Distribution<&T>. This trait system allows rand to support custom types without bloating the core API.
thread_rng() uses lazy initialization. The first call in a thread triggers seeding. rand stores the seeded generator in thread-local storage. Subsequent calls return a handle to the same generator. This avoids the cost of re-seeding. The generator persists for the lifetime of the thread.
Realistic usage: dice, slices, and reproducibility
Randomness appears in many shapes. You might need a dice roll, a random element from a list, or a reproducible sequence for testing.
use rand::Rng;
/// Simulates rolling a 6-sided die.
fn roll_die() -> u32 {
// thread_rng() is efficient to call repeatedly.
// It returns a handle, not a new RNG every time.
let mut rng = rand::thread_rng();
// Range is exclusive on the right.
// 1..7 produces values 1, 2, 3, 4, 5, 6.
rng.gen_range(1..7)
}
/// Picks a random color from a list.
fn pick_color() -> &'static str {
let mut rng = rand::thread_rng();
// gen_range works on slices.
// It selects a random element by index.
let colors = ["red", "green", "blue", "yellow"];
let chosen = rng.gen_range(colors);
chosen
}
/// Generates a random boolean with a 20% chance of true.
fn is_lucky() -> bool {
let mut rng = rand::thread_rng();
// gen_bool takes a probability as a float in [0.0, 1.0].
rng.gen_bool(0.2)
}
Convention aside: gen_range(1..7) and gen_range(1..=6) both produce the same set of values. The community often prefers ..= when the upper bound is a natural limit (like a die face), and .. when the bound represents a count or exclusive ceiling. Pick the form that matches your mental model of the data.
Reproducibility matters for tests. thread_rng() produces different values every run. Tests that depend on random values will flake. Use SeedableRng to create a deterministic generator for test code.
use rand::SeedableRng;
use rand::rngs::StdRng;
/// Demonstrates reproducible randomness for testing.
fn deterministic_demo() {
// StdRng implements SeedableRng.
// from_seed creates a generator with a fixed starting state.
let mut rng = StdRng::seed_from_u64(12345);
// This sequence is identical every time you run this code.
let a = rng.gen_range(1..100);
let b = rng.gen_range(1..100);
println!("A: {a}, B: {b}");
}
Seed your tests. Deterministic tests are the only reliable tests.
Pitfalls and compiler errors
Randomness introduces subtle traps. The compiler catches many of them, but you need to understand the signals.
If you try to generate a random value for a type that doesn't support it, you get E0277 (trait bound not satisfied). rand doesn't know how to randomize your custom struct. You must implement Distribution for the type, or generate fields individually.
use rand::Rng;
struct Point {
x: f64,
y: f64,
}
fn bad_example() {
let mut rng = rand::thread_rng();
// This fails with E0277.
// Point does not implement Distribution.
// let p = rng.gen::<Point>();
// Correct approach: generate fields separately.
let p = Point {
x: rng.gen_range(-10.0..10.0),
y: rng.gen_range(-10.0..10.0),
};
}
thread_rng() returns a ThreadRng which is !Sync. You cannot share it across threads. Each thread gets its own independent generator. This is a safety feature. Sharing mutable state across threads requires synchronization, which kills performance. The thread-local design avoids locks entirely. If you need a shared RNG, use Arc<Mutex<Rng>> or give each thread its own thread_rng(). The latter is almost always better.
Performance pitfalls appear in tight loops. thread_rng() is fast, but it carries overhead for thread-local storage access and cryptographic security. If you profile your code and find random number generation is the bottleneck, switch to SmallRng. SmallRng is a lighter-weight generator designed for speed. It trades cryptographic security for performance.
use rand::rngs::SmallRng;
use rand::SeedableRng;
use rand::Rng;
/// Uses SmallRng for performance-critical loops.
fn fast_loop() {
// SmallRng must be seeded manually.
// thread_rng() can provide the seed.
let mut rng = SmallRng::from_entropy();
for _ in 0..1_000_000 {
let _v = rng.gen_range(0..100);
}
}
Convention aside: SmallRng::from_entropy() seeds the generator using the system entropy source. This is the standard way to initialize SmallRng. Don't hardcode seeds in production code unless you have a specific reason.
Decision matrix
Pick the right tool for the constraint. Speed, security, or reproducibility. Match the generator to the need.
Use thread_rng() when you need fast, secure-enough randomness for general application logic, games, or simulations where reproducibility doesn't matter. Use SeedableRng with a fixed seed when writing tests or debugging simulations that require identical outputs across runs. Use SmallRng when you are generating random numbers in a tight loop and have profiled thread_rng() as a bottleneck; SmallRng is a lighter-weight generator. Use rand::random() when you need a single value of a basic type like bool, u32, or f64 without caring about the range; it's a shorthand for thread_rng().gen(). Reach for rand_distr when you need statistical distributions like normal, exponential, or Poisson; the core rand crate focuses on uniform randomness.