๐ 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. Available Backends (New in 0.5.2! ๐)
Starting from v0.5.2, the library includes multiple built-in cache backend implementations beyond the defaults!
In-Memory Backends (L1 Tier)
| Backend | Feature | Performance | Eviction | Use Case |
|---|---|---|---|---|
| MokaCache (default) | Always available | High | Automatic (LRU + TTL) | Production workloads |
| DashMapCache | Always available | Medium | Manual cleanup | Simple caching, education |
| QuickCacheBackend | backend-quickcache |
Very High | Automatic (LRU) | Maximum throughput |
Distributed Backends (L2 Tier)
| Backend | Feature | Persistence | TTL Introspection | Use Case |
|---|---|---|---|---|
| RedisCache (default) | Always available | Yes (disk) | โ Yes | Production, multi-instance |
| MemcachedCache | backend-memcached |
No (memory only) | โ No | High-performance distributed |
Example: Using DashMapCache as L1
use ;
use Arc;
let dashmap_l1 = new;
let cache = new
.with_l1
.build
.await?;
Example: Using QuickCache (requires feature flag)
[]
= { = "0.5", = ["backend-quickcache"] }
use ;
use Arc;
let quickcache_l1 = new;
let cache = new
.with_l1
.build
.await?;
See Example: examples/builtin_backends.rs for complete demonstrations of all backends.
7. 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
8. 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