A/B Testing in Rust: How Not to Break Production and Find the Truth

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why Rust is Ideal for A/B Tests?

\n

When we talk about A/B testing, Python, R, or JavaScript usually come to mind. But Rust is a dark horse that, in real combat conditions, proves to be faster, safer, and cheaper. If your backend is written in Rust, you can embed experiments directly into the system core without the overhead of microservices and Redis.

\n

Imagine: you change a recommendation algorithm, the color of the "Buy" button, or parsing logic. An error in the A/B test code in Python could bring down the server. In Rust — you simply won't compile if you miss a condition branch. The compiler is your best QA.

\n

In this article, we'll break down A/B testing in Rust from architecture to production. You'll learn how to do split tests without pain, calculate statistics, and not lose users.

\n\n

Architecture of an A/B Test in Rust: Three Pillars

\n

Any experiment in a Rust application rests on three components:

\n
    \n
  • Randomizer — who goes into group A, who into group B?
  • \n
  • Assignment Store — so the user doesn't jump between groups on every request.
  • \n
  • Logging and Statistics — we calculate conversion and p-value.
  • \n
\n

Let's write a simple but working module. No frameworks — just Rust and its standard library.

\n\n

Step 1. Hash-Based Randomization

\n

Deterministic randomization is the key to stability. If you use rand::random(), the user might jump to another group on every server restart. We use a weighted hash of the user ID and experiment name.

\n\n
use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\n#[derive(Debug, Clone, Copy, PartialEq)]\npub enum Group {\n    Control,\n    Treatment,\n}\n\npub fn assign_group(user_id: &str, experiment: &str, treatment_weight: f64) -> Group {\n    let mut hasher = DefaultHasher::new();\n    user_id.hash(&mut hasher);\n    experiment.hash(&mut hasher);\n    let hash = hasher.finish();\n\n    let normalized = (hash as f64) / (u64::MAX as f64);\n    if normalized < treatment_weight {\n        Group::Treatment\n    } else {\n        Group::Control\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_deterministic() {\n        let g1 = assign_group("user_42", "button_color_v2", 0.5);\n        let g2 = assign_group("user_42", "button_color_v2", 0.5);\n        assert_eq!(g1, g2);\n    }\n}\n
\n\n

This code guarantees that the same user will always be in the same group for a specific experiment. The treatment_weight is the fraction of traffic that goes to the test group (e.g., 0.1 = 10%).

\n\n

Step 2. Storage: Cache or Database?

\n

For high-load projects, storing assignments in memory is a risk. If the server crashes, the user loses their group. It's better to use Redis or SQLite. But for simplicity, we'll show an in-memory cache with TTL.

\n\n
use std::collections::HashMap;\nuse std::time::{Duration, Instant};\n\nstruct AssignmentCache {\n    store: HashMap<String, (Group, Instant)>,\n    ttl: Duration,\n}\n\nimpl AssignmentCache {\n    fn new(ttl_secs: u64) -> Self {\n        Self {\n            store: HashMap::new(),\n            ttl: Duration::from_secs(ttl_secs),\n        }\n    }\n\n    fn get(&mut self, key: &str) -> Option<Group> {\n        if let Some((group, time)) = self.store.get(key) {\n            if time.elapsed() < self.ttl {\n                return Some(*group);\n            }\n        }\n        None\n    }\n\n    fn set(&mut self, key: String, group: Group) {\n        self.store.insert(key, (group, Instant::now()));\n    }\n}\n
\n\n

In a real project, replace HashMap with dashmap or Redis — this will provide thread safety and persistence.

\n\n

Integration with

Blogs

Book Recommendations