Redis & Key-Value
Use Redis for caches, rate-limit counters, pub/sub, and ephemeral session state alongside your primary SQL database.
Recipe
Quick-reference recipe card - copy-paste ready.
use redis::AsyncCommands;
let client = redis::Client::open(redis_url)?;
let mut conn = client.get_multiplexed_async_connection().await?;
let _: () = conn.set_ex("user:1:session", session_id, 3600).await?;
let cached: Option<String> = conn.get("user:1:profile").await?;When to reach for this: Read-heavy hot keys, TTL-based eviction, distributed locks, or cross-instance pub/sub - not primary source of truth for money.
Working Example
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Profile { name: String }
async fn cached_profile(
conn: &mut redis::aio::MultiplexedConnection,
user_id: u64,
loader: impl FnOnce() -> Profile,
) -> redis::RedisResult<Profile> {
let key = format!("profile:{user_id}");
if let Ok(json) = conn.get::<_, String>(&key).await {
if let Ok(p) = serde_json::from_str(&json) {
return Ok(p);
}
}
let profile = loader();
let _: () = conn.set_ex(&key, serde_json::to_string(&profile).unwrap(), 300).await?;
Ok(profile)
}What this demonstrates:
- Multiplexed async connection from
rediscrate. - Cache-aside: get JSON, on miss load and
set_exwith TTL. - Serde JSON as value encoding convention.
- Key naming with colon namespaces.
Deep Dive
How It Works
- Redis is in-memory with optional persistence (RDB/AOF).
- TTL keys auto-expire - ideal for sessions and caches.
- Single-threaded server - avoid huge values and hot key storms.
Common Patterns
| Pattern | Redis type | Use |
|---|---|---|
| Cache | String (JSON) | Profile, config |
| Rate limit | String INCR | Per-IP counters |
| Lock | String SET NX | Short critical sections |
| Queue | List / Stream | Simple job buffer |
| Pub/Sub | Channels | Cache invalidation fan-out |
Rust Notes
// fred or deadpool-redis for connection pooling at scaleGotchas
- Cache as source of truth - Data loss on flush loses money records. Fix: SQL authoritative; Redis cache only.
- No TTL - Keys grow until OOM. Fix: Always set TTL on cache entries.
- Thundering herd - Cache miss stampede on hot key. Fix: Single-flight lock or stale-while-revalidate.
- Large JSON blobs - Blocks Redis single thread. Fix: Keep values small; chunk or use SQL for big payloads.
- Pub/sub reliability - Fire-and-forget, no persistence. Fix: Use Redis Streams or message broker for jobs.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| In-process moka cache | Single instance, microsecond latency | Multi-instance consistency needed |
| Memcached | Simple TTL cache only | Pub/sub, structures, persistence |
| PostgreSQL UNLOGGED | Small team, one database | Dedicated cache tier already exists |
FAQs
Which Rust crate?
redis crate with async features; fred for cluster-heavy setups.
Connection pool?
deadpool-redis or multiplexed connection per process.
Cluster mode?
Use cluster-aware client; hash tags for multi-key ops.
TLS?
rediss:// URL with rustls feature on client.
Serialization?
JSON for debuggability; MessagePack for size at scale.
Invalidate cache?
Pub/sub channel or version suffix in key on writes.
Rate limiting?
INCR with EXPIRE window - see Rate Limiting page.
Session store?
Random session ID key with TTL; store user ID as value.
Testing?
testcontainers Redis module or embedded mock implementing trait.
GDPR delete?
Track key patterns per user; delete by SCAN prefix on account erasure.
Related
- Connection Pooling - Redis pools
- Databases Basics - SQL + cache split
- Rate Limiting & Abuse Prevention - Redis counters
- Authentication & Sessions - Session storage
- Databases Best Practices - Cache discipline
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.