multi-tier-cache
A high-performance, production-ready multi-tier caching library for Rust.
Combine blazing-fast in-memory caching (Moka / QuickCache) with persistent distributed storage (Redis / Memcached) in a single, unified API — with stampede protection, cross-instance invalidation, and dynamic N-tier architectures out of the box.
Request ─→ L1 (RAM) ─→ L2 (Redis) ─→ L3 (Cold) ─→ … ─→ Compute
<1ms 2-5ms 10-50ms your latency
Highlights
- Multi-tier by default — L1 + L2 out of the box, extensible to L3, L4, … LN with per-tier TTL scaling
- Stampede protection — broadcast-channel request coalescing prevents thundering herds (99.6% latency reduction)
- Cross-instance invalidation — real-time cache sync across servers via Redis Pub/Sub
- Pluggable backends — swap Moka, Redis, DashMap, QuickCache, Memcached, or bring your own
- Type-safe compute-on-miss —
get_or_compute_typed::<T>()handles serialization automatically - Redis Streams — built-in pub/sub with automatic trimming for event-driven architectures
- Zero-cost L1 hits — raw
bytes::Bytesstorage, no intermediate JSON AST allocations - Probabilistic promotion — configurable 1/N promotion frequency to avoid L1 pollution
- Production-proven — battle-tested at 21,528+ RPS with 19.0ms p50 latency and 95% hit rate
Table of Contents
- Quick Start
- Feature Flags
- Cache Strategies
- Compute-on-Miss
- Type-Safe Caching
- Cross-Instance Invalidation
- Multi-Tier Architecture
- Available Backends
- Custom Backends
- Redis Streams
- Error Handling
- Configuration
- Performance
- Testing
- Examples
- Migration Guide
- Contributing
- License
Quick Start
Add to your Cargo.toml:
[]
= "0.6"
= { = "1", = ["full"] }
use ;
async
Note: By default the library connects to Redis at
redis://127.0.0.1:6379. Set theREDIS_URLenvironment variable or useCacheSystem::with_redis_url()to override. See Configuration for details.
Feature Flags
All backends are optional and feature-gated to keep the dependency tree lean.
| Feature | Description | Default |
|---|---|---|
moka |
Moka in-memory cache (L1) | ✅ |
redis |
Redis distributed cache (L2) | ✅ |
backend-quickcache |
QuickCache ultra-fast L1 backend | — |
backend-memcached |
Memcached distributed L2 backend | — |
bincode |
Bincode binary serialization | — |
msgpack |
MessagePack serialization | — |
full |
Enable everything above | — |
# Default — Moka L1 + Redis L2
= "0.6"
# In-memory only (no Redis dependency)
= { = "0.6", = false, = ["moka"] }
# All backends + all serializers
= { = "0.6", = ["full"] }
Cache Strategies
Built-in TTL presets for common use cases:
use CacheStrategy;
use Duration;
// Predefined strategies
RealTime // 10 seconds — live prices, counters
ShortTerm // 5 minutes — sessions, hot data
MediumTerm // 1 hour — catalogs, API responses
LongTerm // 3 hours — config, stable data
// Or specify your own
Custom
cache.cache_manager
.set_with_strategy
.await?;
Compute-on-Miss
Fetch data only on cache miss. Concurrent requests for the same key are coalesced — only one computation runs, the rest wait and share the result.
let value = cache.cache_manager
.get_or_compute_with
.await?;
Type-Safe Caching
Skip manual serialization entirely. get_or_compute_typed handles Serialize/DeserializeOwned for you:
use ;
let user: User = cache.cache_manager
.get_or_compute_typed
.await?;
Works with any T: Serialize + DeserializeOwned — database rows, API responses, computed analytics, etc.
Cross-Instance Invalidation
In distributed deployments, L1 caches on different servers can become stale. The invalidation system uses Redis Pub/Sub to synchronize all instances in real time.
Setup
use ;
use Arc;
let cache_manager = new_with_invalidation.await?;
Strategies
Remove — evict and reload lazily on next access:
database.update_user.await?;
cache_manager.invalidate.await?;
Update — push new data to all instances (zero cache miss):
let bytes = from;
cache_manager.update_cache.await?;
Pattern — invalidate multiple related keys at once:
cache_manager.invalidate_pattern.await?;
Write-through — cache locally and broadcast in one call:
cache_manager.set_with_broadcast.await?;
Propagation flow
Instance A Redis Pub/Sub Instance B
│ update data │ │
│ broadcast ─────────>│ │
│ │ deliver ──────────>│
│ │ update L1
| Strategy | Bandwidth | Cache Miss on Next Read | Best For |
|---|---|---|---|
| Remove | Low | Yes | Large values, infrequent reads |
| Update | Higher | No | Small values, frequent reads |
| Pattern | Medium | Yes | Bulk invalidation |
Configuration
let config = InvalidationConfig ;
Multi-Tier Architecture
Go beyond L1 + L2. Add L3 (cold storage), L4 (archive), or any number of tiers — each with its own TTL multiplier and promotion policy.
L1 (RAM) → L2 (Redis) → L3 (RocksDB) → L4 (S3)
<1ms 2-5ms 10-50ms 100-500ms
95% hits 4% 0.9% 0.1%
Building a 3-tier cache
use ;
let cache = new
.with_tier
.with_tier
.with_l3 // convenience: 2× TTL
.build
.await?;
Tier presets
| Preset | Promotion | TTL Scale | Purpose |
|---|---|---|---|
TierConfig::as_l1() |
— | 1× | Hot data in RAM |
TierConfig::as_l2() |
→ L1 | 1× | Warm distributed |
TierConfig::as_l3() |
→ L2 → L1 | 2× | Cold storage |
TierConfig::as_l4() |
→ all | 8× | Archive |
Custom tiers:
new
.with_promotion
.with_ttl_scale
.with_promotion_frequency // promote 1 in 20 hits
TTL scaling example
Setting CacheStrategy::MediumTerm (1 hour):
| Tier | Scale | Effective TTL |
|---|---|---|
| L1 | 1× | 1 h |
| L2 | 1× | 1 h |
| L3 | 2× | 2 h |
| L4 | 8× | 8 h |
Automatic promotion
When data is found in a lower tier, it is promoted upward automatically:
GET "key"
├─ L1 → miss
├─ L2 → miss
└─ L3 → HIT → promote to L2 → promote to L1 → return
Per-tier statistics
if let Some = cache.cache_manager.get_tier_stats
Backward compatibility
Existing 2-tier code works without changes. Multi-tier mode is opt-in via .with_tier().
// Still works exactly as before
let cache = new.build.await?;
Available Backends
In-Memory (L1)
| Backend | Feature | Eviction | Notes |
|---|---|---|---|
| MokaCache | moka (default) |
Automatic (LRU + TTL) | Production recommended |
| DashMapCache | always available | Manual cleanup | Simple, no eviction policy |
| QuickCacheBackend | backend-quickcache |
Automatic (LRU) | Maximum throughput |
Distributed (L2)
| Backend | Feature | Persistence | TTL Introspection |
|---|---|---|---|
| RedisCache | redis (default) |
Yes | ✅ |
| MemcachedCache | backend-memcached |
No | ❌ |
Usage
use ;
use Arc;
let cache = new
.with_l1
.build
.await?;
QuickCache (requires feature flag):
= { = "0.6", = ["backend-quickcache"] }
use ;
let cache = new
.with_l1
.build
.await?;
Custom Backends
Implement CacheBackend for L1, or L2CacheBackend (extends CacheBackend with get_with_ttl) for L2.
L1 example
use ;
use Bytes;
use HashMap;
use ;
use ;
L2 example
use L2CacheBackend;
Builder API
let cache = new
.with_l1 // any Arc<dyn CacheBackend>
.with_l2 // any Arc<dyn L2CacheBackend>
.with_streams // optional Arc<dyn StreamingBackend>
.build
.await?;
See examples/custom_backends.rs for complete working examples.
Redis Streams
Built-in publish/subscribe with automatic trimming:
// Publish an event
let fields = vec!;
cache.cache_manager
.publish_to_stream
.await?;
// Read latest entries
let entries = cache.cache_manager
.read_stream_latest
.await?;
// Blocking read for new entries (5s timeout)
let new = cache.cache_manager
.read_stream
.await?;
Error Handling
All operations return CacheResult<T>, powered by a structured CacheError enum:
use CacheError;
match cache.cache_manager.get.await
CacheError implements From for RedisError, serde_json::Error, MemcacheError, and more — so ? works seamlessly.
Configuration
Redis Connection
| Priority | Method |
|---|---|
| 1 | CacheSystem::with_redis_url("redis://…") |
| 2 | REDIS_URL environment variable |
| 3 | .env file |
| 4 | Default: redis://127.0.0.1:6379 |
# Shell
# .env file
REDIS_URL="redis://localhost:6379"
Redis URL format:
redis://[username]:[password]@[host]:[port]/[database]
rediss://… # TLS connection
Moka L1 Configuration
use ;
use Duration;
let config = MokaCacheConfig ;
let cache = new
.with_moka_config
.build
.await?;
Default Tuning
| Parameter | Default |
|---|---|
| L1 capacity | 2 000 entries |
| L1 TTL | 5 min (per key) |
| L2 TTL | 1 h (per key) |
| Stream max length | 1 000 entries |
Docker Compose
services:
app:
environment:
- REDIS_URL=redis://redis:6379
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Performance
Benchmarked in production (Google Cloud e2-micro):
| Metric | Value |
|---|---|
| Throughput | 21,528+ req/s |
| Latency (p50) | 19.0 ms |
| Latency (mean) | 23.2 ms |
| Cache hit rate | 95%+ |
| Stampede reduction | 99.6% |
| Error rate | 0% (50k requests) |
Comparison
| Library | Multi-Tier | Stampede Protection | Redis | Streams | Invalidation |
|---|---|---|---|---|---|
| multi-tier-cache | ✅ N-tier | ✅ broadcast | ✅ | ✅ | ✅ Pub/Sub |
| cached | — | — | — | — | — |
| moka | L1 only | L1 only | — | — | — |
| redis-rs | — | manual | ✅ | manual | manual |
Running Benchmarks
HTML reports are saved to target/criterion/.
Testing
Integration tests run against a real Redis instance:
Requirements: Redis on localhost:6379 (or set REDIS_URL). Tests clean up after themselves.
Coverage: L1/L2 get/set/remove, TTL, promotion, stampede protection, cross-instance invalidation (remove, update, pattern), type-safe caching, Redis Streams, statistics.
Examples
Migration Guide
From cached
// Before (cached)
// After (multi-tier-cache)
let result = cache.cache_manager
.get_or_compute_with
.await?;
From redis-rs
// Before
let value: String = conn.get?;
conn.set_ex?;
// After
let value = cache.cache_manager.get.await?;
cache.cache_manager
.set_with_strategy
.await?;
Feature Compatibility
| Feature | Default Redis L2 | Custom L2 Backend |
|---|---|---|
| Single-key invalidation | ✅ | ✅ |
| Pattern invalidation | ✅ | ⚠️ not available |
| Type-safe caching | ✅ | ✅ |
| Stampede protection | ✅ | ✅ |
| Streaming | ✅ | requires StreamingBackend |
Note: Pattern-based invalidation (
invalidate_pattern) requires the concreteRedisCacheL2 backend forSCANsupport.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. See CONTRIBUTING.md for guidelines.
License
Licensed under either of:
at your option.
Acknowledgments
Built on top of Moka, redis-rs, DashMap, and Tokio.
Made with ❤️ in Rust · Production-proven in crypto trading at 21,528+ RPS