๐ multi-tier-cache
A high-performance, production-ready multi-tier caching library for Rust featuring L1 (in-memory) + L2 (Redis) caches, automatic stampede protection, and built-in Redis Streams support.
๐ Table of Contents
- Features
- Architecture
- Installation
- Quick Start
- Usage Patterns
- Feature Compatibility
- Performance Benchmarks
- Configuration
- Testing
- Examples
- Architecture Details
- Development
- Contributing
- License
โจ Features
- ๐ฅ Multi-Tier Architecture: Combines fast in-memory (Moka) with persistent distributed (Redis) caching
- ๐ Dynamic Multi-Tier (v0.5.0+): Support for 3, 4, or more cache tiers (L1+L2+L3+L4+...) with flexible configuration โญ NEW
- ๐ Cross-Instance Cache Invalidation (v0.4.0+): Real-time cache synchronization across all instances via Redis Pub/Sub
- ๐ Pluggable Backends (v0.3.0+): Swap Moka/Redis with custom implementations (DashMap, Memcached, RocksDB, etc.)
- ๐ก๏ธ Cache Stampede Protection: DashMap + Mutex request coalescing prevents duplicate computations (99.6% latency reduction: 534ms โ 5.2ms)
- ๐ Redis Streams: Built-in publish/subscribe with automatic trimming for event streaming
- โก Automatic Tier Promotion: Intelligent cache tier promotion for frequently accessed data with TTL preservation and per-tier scaling
- ๐ Comprehensive Statistics: Hit rates per tier, promotions, in-flight request tracking, invalidation metrics
- ๐ฏ Zero-Config: Sensible defaults, works out of the box
- โ Production-Proven: Battle-tested at 16,829+ RPS with 5.2ms latency and 95% hit rate
๐๏ธ Architecture
Request โ L1 Cache (Moka) โ L2 Cache (Redis) โ Compute/Fetch
โ Hit (90%) โ Hit (75%) โ Miss (5%)
Return Promote to L1 Store in L1+L2
Cache Flow
- Fast Path: Check L1 cache (sub-millisecond, 90% hit rate)
- Fallback: Check L2 cache (2-5ms, 75% hit rate) + auto-promote to L1
- Compute: Fetch/compute fresh data with stampede protection, store in both tiers
๐ฆ Installation
Add to your Cargo.toml:
[]
= "0.5"
= { = "1.28", = ["full"] }
= "1.0"
Version Guide:
- v0.5.0+: Dynamic multi-tier architecture (L1+L2+L3+L4+...), per-tier statistics โญ NEW
- v0.4.0+: Cross-instance cache invalidation via Redis Pub/Sub
- v0.3.0+: Pluggable backends, trait-based architecture
- v0.2.0+: Type-safe database caching with
get_or_compute_typed() - v0.1.0+: Core multi-tier caching with stampede protection
๐ Quick Start
use ;
async
๐ก Usage Patterns
1. Cache Strategies
Choose the right TTL for your use case:
use Duration;
// RealTime (10s) - Fast-changing data
cache.cache_manager
.set_with_strategy
.await?;
// ShortTerm (5min) - Frequently accessed data
cache.cache_manager
.set_with_strategy
.await?;
// MediumTerm (1hr) - Moderately stable data
cache.cache_manager
.set_with_strategy
.await?;
// LongTerm (3hr) - Stable data
cache.cache_manager
.set_with_strategy
.await?;
// Custom - Specific requirements
cache.cache_manager
.set_with_strategy
.await?;
2. Compute-on-Miss Pattern
Fetch data only when cache misses, with stampede protection:
async
// Only ONE request will compute, others wait and read from cache
let product = cache.cache_manager
.get_or_compute_with
.await?;
3. Redis Streams Integration
Publish and consume events:
// Publish to stream
let fields = vec!;
let entry_id = cache.cache_manager
.publish_to_stream // Auto-trim to 1000 entries
.await?;
// Read latest entries
let entries = cache.cache_manager
.read_stream_latest
.await?;
// Blocking read for new entries
let new_entries = cache.cache_manager
.read_stream // Block for 5s
.await?;
4. Type-Safe Database Caching (New in 0.2.0! ๐)
Eliminate boilerplate with automatic serialization/deserialization for database queries:
use ;
// โ OLD WAY: Manual cache + serialize + deserialize (40+ lines)
let cached = cache.cache_manager.get.await?;
let user: User = match cached ;
// โ
NEW WAY: Type-safe automatic caching (5 lines)
let user: User = cache.cache_manager
.get_or_compute_typed
.await?;
Benefits:
- โ Type-Safe: Compiler checks types, no runtime surprises
- โ Zero Boilerplate: Automatic serialize/deserialize
- โ Full Cache Features: L1โL2 fallback, stampede protection, auto-promotion
- โ
Generic: Works with any type implementing
Serialize + DeserializeOwned
More Examples:
// PostgreSQL Reports
let report: Report = cache.cache_manager
.get_or_compute_typed
.await?;
// API Responses
let data: ApiData = cache.cache_manager
.get_or_compute_typed
.await?;
// Complex Computations
let analytics: AnalyticsResult = cache.cache_manager
.get_or_compute_typed
.await?;
Performance:
- L1 Hit: <1ms + deserialization (~10-50ฮผs)
- L2 Hit: 2-5ms + deserialization + L1 promotion
- Cache Miss: Your query time + serialization + L1+L2 storage
5. Cross-Instance Cache Invalidation (New in 0.4.0! ๐)
Keep caches synchronized across multiple servers/instances using Redis Pub/Sub:
Why Invalidation?
In distributed systems with multiple cache instances, stale data is a common problem:
- User updates profile on Server A โ Cache on Server B still has old data
- Admin changes product price โ Other servers show outdated prices
- TTL-only expiration โ Users see stale data until timeout
Solution: Real-time cache invalidation across ALL instances!
Two Invalidation Strategies
1. Remove Strategy (Lazy Reload)
use ;
// Initialize with invalidation support
let config = default;
let cache_manager = new_with_invalidation.await?;
// Update database
database.update_user.await?;
// Invalidate cache across ALL instances
// โ Cache removed, next access triggers reload
cache_manager.invalidate.await?;
2. Update Strategy (Zero Cache Miss)
// Update database
database.update_user.await?;
// Push new data directly to ALL instances' L1 caches
// โ No cache miss, instant update!
cache_manager.update_cache.await?;
Pattern-Based Invalidation
Invalidate multiple related keys at once:
// Update product category in database
database.update_category.await?;
// Invalidate ALL products in category across ALL instances
cache_manager.invalidate_pattern.await?;
Write-Through Caching
Cache and broadcast in one operation:
let report = generate_monthly_report.await?;
// Cache locally AND broadcast to all other instances
cache_manager.set_with_broadcast.await?;
How It Works
Instance A Redis Pub/Sub Instance B
โ โ โ
โ 1. Update data โ โ
โ 2. Broadcast msg โโโ>โ โ
โ โ 3. Receive msg โโโ>โ
โ โ 4. Update L1 โโโโโ
โ โ โ
Performance:
- Latency: ~1-5ms invalidation propagation
- Overhead: Negligible (<0.1% CPU for subscriber)
- Production-Safe: Auto-reconnection, error recovery
Configuration
use InvalidationConfig;
let config = InvalidationConfig ;
When to Use:
- โ Multi-server deployments (load balancers, horizontal scaling)
- โ Data that changes frequently (user profiles, prices, inventory)
- โ Real-time requirements (instant consistency)
- โ Single-server deployments (unnecessary overhead)
- โ Rarely-changing data (TTL is sufficient)
Comparison:
| Strategy | Bandwidth | Cache Miss | Use Case |
|---|---|---|---|
| Remove | Low | Yes (on next access) | Large values, infrequent access |
| Update | Higher | No (instant) | Small values, frequent access |
| Pattern | Medium | Yes | Bulk invalidation (categories) |
6. Custom Cache Backends (New in 0.3.0! ๐)
Starting from v0.3.0, you can replace the default Moka (L1) and Redis (L2) backends with your own custom implementations!
Use Cases:
- Replace Redis with Memcached, DragonflyDB, or KeyDB
- Use DashMap instead of Moka for L1
- Implement no-op caches for testing
- Add custom cache eviction policies
- Integrate with proprietary caching systems
Basic Example: Custom HashMap L1 Cache
use ;
use HashMap;
use ;
use ;
use Result;
// Use custom backend
let custom_l1 = new;
let cache = new
.with_l1
.build
.await?;
Advanced: Custom L2 Backend with TTL
For L2 caches, implement L2CacheBackend which extends CacheBackend with get_with_ttl():
use ;
Builder API
use CacheSystemBuilder;
let cache = new
.with_l1 // Custom L1 backend
.with_l2 // Custom L2 backend
.with_streams // Optional: Custom streaming backend
.build
.await?;
Mix and Match:
- Use custom L1 with default Redis L2
- Use default Moka L1 with custom L2
- Replace both L1 and L2 backends
See: examples/custom_backends.rs for complete working examples including:
- HashMap L1 cache
- In-memory L2 cache with TTL
- No-op cache (for testing)
- Mixed backend configurations
7. Multi-Tier Architecture (New in 0.5.0! ๐)
Starting from v0.5.0, you can configure 3, 4, or more cache tiers beyond the default L1+L2 setup!
Use Cases:
- L3 (Cold Storage): RocksDB or LevelDB for large datasets with longer TTL
- L4 (Archive): S3 or filesystem for rarely-accessed but important data
- Custom Tiers: Any combination of backends to fit your workload
Why Multi-Tier?
Request โ L1 (Hot - RAM) โ L2 (Warm - Redis) โ L3 (Cold - RocksDB) โ L4 (Archive - S3)
<1ms (95%) 2-5ms (4%) 10-50ms (0.9%) 100-500ms (0.1%)
- Cost Optimization: Keep hot data in expensive fast storage, cold data in cheap slow storage
- Capacity: Extend cache capacity beyond RAM limits
- Performance: 95%+ requests served from L1/L2, only rare misses hit slower tiers
Basic Example: 3-Tier Cache
use ;
use Arc;
async
Tier Configuration
Pre-configured Tiers:
// L1 - Hot tier (no promotion, standard TTL)
as_l1
// L2 - Warm tier (promote to L1, standard TTL)
as_l2
// L3 - Cold tier (promote to L2+L1, 2x TTL)
as_l3
// L4 - Archive tier (promote to all, 8x TTL)
as_l4
Custom Tier:
new
.with_promotion // Auto-promote on hit
.with_ttl_scale // 5x TTL multiplier
.with_level // Tier number
TTL Scaling Example
// Set data with 1-hour TTL
cache.cache_manager
.set_with_strategy // 1hr
.await?;
// Actual TTL per tier:
// L1: 1 hour (scale = 1.0x)
// L2: 1 hour (scale = 1.0x)
// L3: 2 hours (scale = 2.0x) โ Keeps data longer!
// L4: 8 hours (scale = 8.0x) โ Much longer retention!
Per-Tier Statistics
Track hit rates for each tier:
if let Some = cache.cache_manager.get_tier_stats
// Output:
// L1: 9500 hits (Redis)
// L2: 450 hits (Redis)
// L3: 45 hits (RocksDB)
// L4: 5 hits (S3)
4-Tier Example
let cache = new
.with_tier
.with_tier
.with_tier
.with_tier
.build
.await?;
Automatic Tier Promotion
When data is found in a lower tier (e.g., L3), it's automatically promoted to all upper tiers:
Request for "key"
โโ Check L1 โ Miss
โโ Check L2 โ Miss
โโ Check L3 โ HIT!
โโ Promote to L2 (with original TTL)
โโ Promote to L1 (with original TTL)
โโ Return data
Next request for "key" โ L1 Hit! <1ms
Backward Compatibility
Existing 2-tier users: No changes required! Your code continues to work:
// This still works exactly as before (v0.1.0 - v0.4.x)
let cache = new.build.await?;
Multi-tier mode is opt-in via .with_tier() or .with_l3()/.with_l4() methods.
When to Use Multi-Tier
โ Good fit:
- Large datasets that don't fit in RAM
- Cost-sensitive workloads (mix expensive + cheap storage)
- Long-tail data access patterns (90% hot, 10% cold)
- Hierarchical data with different access frequencies
โ Not needed:
- Small datasets (< 10GB) that fit in Redis
- Uniform access patterns (all data equally hot)
- Latency-critical paths (stick to L1+L2)
โ๏ธ Feature Compatibility
Invalidation + Custom Backends
โ Compatible:
- Cache invalidation works with default Redis L2 backend
- Single-key operations (
invalidate,update_cache) work with any backend - Type-safe caching works with all backends
- Stampede protection works with all backends
โ ๏ธ Limited Support:
- Pattern-based invalidation (
invalidate_pattern) requires concrete Redis L2Cache - Custom L2 backends: Single-key invalidation works, but pattern invalidation not available
- Workaround: Implement pattern matching in your custom backend
Example:
// โ
Works: Default Redis + Invalidation
let cache = new_with_invalidation.await?;
cache.invalidate.await?; // โ
Works
cache.invalidate_pattern.await?; // โ
Works (has scan_keys)
// โ ๏ธ Limited: Custom L2 + Invalidation
let cache = new_with_backends.await?;
// Pattern invalidation not available without concrete L2Cache
// Use single-key invalidation instead
Combining All Features
All features work together seamlessly:
use *;
// v0.4.0: Invalidation
let config = default;
// v0.3.0: Custom backends (or use defaults)
let l1 = new;
let l2 = new;
// Initialize with invalidation
let cache_manager = new_with_invalidation.await?;
// v0.2.0: Type-safe caching
let user: User = cache_manager.get_or_compute_typed.await?;
// v0.4.0: Invalidate across instances
cache_manager.invalidate.await?;
// v0.1.0: All core features work
let stats = cache_manager.get_stats;
println!;
No Conflicts: All features are designed to work together without interference.
๐ Performance Benchmarks
Tested in production environment:
| Metric | Value |
|---|---|
| Throughput | 16,829+ requests/second |
| Latency (p50) | 5.2ms |
| Cache Hit Rate | 95% (L1: 90%, L2: 75%) |
| Stampede Protection | 99.6% latency reduction (534ms โ 5.2ms) |
| Success Rate | 100% (zero failures under load) |
Comparison with Other Libraries
| Library | Multi-Tier | Stampede Protection | Redis Support | Streams | Invalidation |
|---|---|---|---|---|---|
| multi-tier-cache | โ L1+L2 | โ Full | โ Full | โ Built-in | โ Pub/Sub |
| cached | โ Single | โ No | โ No | โ No | โ No |
| moka | โ L1 only | โ L1 only | โ No | โ No | โ No |
| redis-rs | โ No cache | โ Manual | โ Low-level | โ Manual | โ Manual |
Running Benchmarks
The library includes comprehensive benchmarks built with Criterion:
# Run all benchmarks
# Run specific benchmark suite
# Generate detailed HTML reports
Benchmark Suites:
- cache_operations: L1/L2 read/write performance, cache strategies, compute-on-miss patterns
- stampede_protection: Concurrent access, request coalescing under load
- invalidation: Cross-instance invalidation overhead, pattern matching performance
- serialization: JSON vs typed caching, data size impact
Results are saved to target/criterion/ with interactive HTML reports.
๐ง Configuration
Redis Connection (REDIS_URL)
The library connects to Redis using the REDIS_URL environment variable. Configuration priority (highest to lowest):
1. Programmatic Configuration (Highest Priority)
// Set custom Redis URL before initialization
let cache = with_redis_url.await?;
2. Environment Variable
# Set in shell
3. .env File (Recommended for Development)
# Create .env file in project root
REDIS_URL="redis://localhost:6379"
4. Default Fallback
If not configured, defaults to: redis://127.0.0.1:6379
Use Cases
Development (Local Redis)
# .env
REDIS_URL="redis://127.0.0.1:6379"
Production (Cloud Redis with Authentication)
# Railway, Render, AWS ElastiCache, etc.
REDIS_URL="redis://:your-password@redis-host.cloud:6379"
Docker Compose
services:
app:
environment:
- REDIS_URL=redis://redis:6379
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Testing (Separate Instance)
async
Redis URL Format
redis://[username]:[password]@[host]:[port]/[database]
Examples:
redis://localhost:6379- Local Redis, no authenticationredis://:mypassword@localhost:6379- Local with password onlyredis://user:pass@redis.example.com:6379/0- Remote with username, password, and database 0rediss://redis.cloud:6380- SSL/TLS connection (note therediss://)
Troubleshooting Redis Connection
Connection Refused
# Check if Redis is running
# Check the port
|
# Verify REDIS_URL
Authentication Failed
# Ensure password is in the URL
REDIS_URL="redis://:YOUR_PASSWORD@host:6379"
# Test connection with redis-cli
Timeout Errors
- Check network connectivity:
ping your-redis-host - Verify firewall rules allow port 6379
- Check Redis
maxclientssetting (may be full) - Review Redis logs:
redis-cli INFO clients
DNS Resolution Issues
# Test DNS resolution
# Use IP address as fallback
REDIS_URL="redis://192.168.1.100:6379"
Cache Tuning
Default settings (configurable in library source):
- L1 Capacity: 2000 entries
- L1 TTL: 5 minutes (per key)
- L2 TTL: 1 hour (per key)
- Stream Max Length: 1000 entries
๐งช Testing
Integration Tests
The library includes comprehensive integration tests (30 tests) that verify functionality with real Redis:
# Run all integration tests
# Run specific test suite
Test Coverage:
- โ L1 cache operations (get, set, remove, TTL)
- โ L2 cache operations (get_with_ttl, scan_keys, bulk operations)
- โ L2-to-L1 promotion
- โ Cross-instance invalidation (Remove, Update, Pattern)
- โ Stampede protection with concurrent requests
- โ Type-safe caching with serialization
- โ Redis Streams (publish, read, trimming)
- โ Statistics tracking
Requirements:
- Redis server running on
localhost:6379(or setREDIS_URL) - Tests automatically clean up after themselves
Test Structure:
tests/
โโโ common/mod.rs # Shared utilities
โโโ integration_basic.rs # Core cache operations
โโโ integration_invalidation.rs # Cross-instance sync
โโโ integration_stampede.rs # Concurrent access
โโโ integration_streams.rs # Redis Streams
๐ Examples
Run examples with:
# Basic usage
# Stampede protection demonstration
# Redis Streams
# Cache strategies
# Advanced patterns
# Health monitoring
๐๏ธ Architecture Details
Cache Stampede Protection
When multiple requests hit an expired cache key simultaneously:
- First request acquires DashMap mutex lock and computes value
- Subsequent requests wait on the same mutex
- After computation, all requests read from cache
- Result: Only ONE computation instead of N
Performance Impact:
- Without protection: 10 requests ร 500ms = 5000ms total
- With protection: 1 request ร 500ms = 500ms total (90% faster)
L2-to-L1 Promotion
When data is found in L2 but not L1:
- Retrieve from Redis (L2)
- Automatically store in Moka (L1) with fresh TTL
- Future requests hit fast L1 cache
- Result: Self-optimizing cache that adapts to access patterns
๐ ๏ธ Development
Build
# Development
# Release (optimized)
# Run tests
Documentation
# Generate and open docs
๐ Migration Guide
From cached crate
// Before (cached)
use cached;
// After (multi-tier-cache)
async
From direct Redis usage
// Before (redis-rs)
let mut conn = client.get_connection?;
let value: String = conn.get?;
conn.set_ex?;
// After (multi-tier-cache)
if let Some = cache.cache_manager.get.await?
cache.cache_manager
.set_with_strategy
.await?;
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
๐ License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
๐ Acknowledgments
Built with:
- Moka - High-performance concurrent cache library
- Redis-rs - Redis client for Rust
- DashMap - Blazingly fast concurrent map
- Tokio - Asynchronous runtime
๐ Contact
- GitHub Issues: Report bugs or request features
Made with โค๏ธ in Rust | Production-proven in crypto trading dashboard serving 16,829+ RPS