Concurrent in-memory cache with per-key TTL and value deduplication — identical payloads share one Arc<V> across keys.
Built for workloads where memory matters: large or repeated values, many keys pointing at the same blob. One dependency (parking_lot).
- Large values — configs, JSON blobs, file contents, model weights, serialized structs
- High duplicate rate — many keys referencing the same payload (aliases, content-addressed keys, fan-out caches)
- Per-key TTL — session tokens, cached API responses with mixed lifetimes
- Concurrent access — sharded reads/writes without pulling in a heavier cache stack
- Tiny or cheap values —
u64, small enums,Copytypes. Interning addsArcoverhead, hashing, and pool bookkeeping that costs more than storing inline. - Mostly unique values — if every key owns a distinct payload, dedup never fires and you pay for the intern pool for nothing. Prefer moka or quick_cache. If you are not sure which side of this line your workload falls on, the dedup monitor measures it and will tell you.
- Exact LRU or advanced admission — eviction is sampled approximate-LRU; no W-TinyLFU-style policy yet.
- Proactive expiry below capacity — expired entries are removed on
getand during at-capacity inserts, not by a background sweeper.
See docs/architecture.md for internals.
Basic insert, TTL, and dedup
use std::sync::Arc;
use std::time::Duration;
use dedup_cache::{Cache, CacheConfig};
let cache = Cache::new(CacheConfig::new(10_000).with_num_shards(16));
cache.insert_with_ttl("session:abc".into(), r#"{"user":1}"#.into(), Some(Duration::from_secs(300)));
cache.insert("config:theme".into(), "dark".into());
// Same payload under two keys → one Arc in memory
let blob = r#"{"shared":true}"#.to_string();
cache.insert("alias:a".into(), blob.clone());
cache.insert("alias:b".into(), blob);
assert!(Arc::ptr_eq(
&cache.get(&"alias:a".to_string()).unwrap(),
&cache.get(&"alias:b".to_string()).unwrap(),
));Default TTL with per-key override
use std::time::Duration;
use dedup_cache::{Cache, CacheConfig};
let cache = Cache::new(
CacheConfig::new(10_000)
.with_num_shards(16)
.with_default_ttl(Duration::from_secs(60)),
);
cache.insert("user:1".into(), r#"{"name":"Ada"}"#.to_string());
cache.insert_with_ttl("token".into(), "secret-token".to_string(), Some(Duration::from_secs(5)));Concurrent use
use std::sync::Arc;
use std::thread;
use dedup_cache::{Cache, CacheConfig};
let cache = Arc::new(Cache::new(CacheConfig::new(50_000).with_num_shards(64)));
thread::spawn({
let cache = Arc::clone(&cache);
move || cache.insert("key".into(), "computed-payload".to_string())
});
if let Some(v) = cache.get(&"key".to_string()) {
// shared Arc — cheap to clone, safe across threads
}Deduplication only helps when values are both large and shared. An opt-in monitor measures whether that is true for your workload — and nets out what the intern pool itself costs, so the answer is in bytes rather than a ratio that cannot tell 20-byte values from 2 MB blobs.
use std::time::Duration;
use dedup_cache::{Cache, CacheConfig, DedupMonitor, DedupVerdict};
let cache: Cache<String, String> = Cache::with_monitor(
CacheConfig::new(100_000),
DedupMonitor::new(|value: &String| value.len())
// Warn at most once an hour, checked every 65,536 inserts.
.with_check_every_inserts(65_536)
.with_min_interval(Duration::from_secs(3_600))
.with_sink(|report| eprintln!("{}", report.advice().unwrap_or_default())),
);
cache.insert("alias:a".into(), "payload".to_string());
// Or pull the numbers whenever you like, warning or not.
let report = cache.dedup_report();
match report.verdict {
DedupVerdict::Counterproductive { net_cost_bytes } => {
// The intern pool costs more than the duplication it removes.
}
DedupVerdict::Effective { net_saved_bytes } => {}
_ => {}
}Without a monitor there is no byte accounting and no weigher is ever called; dedup_report() still returns exact entry and distinct-value counts, and stats().dedup_ratio() is always free. Enabling the monitor costs about 30 ns per insert for small values and is lost in the noise for large ones. Set .with_check_every_inserts(0) for pull-only reporting with no background work at all, and enable the log-warnings feature plus .with_log_sink() to route warnings through the log crate.
cargo run --example basic
cargo run --example concurrent_session_cache --release
cargo run --example dedup_monitor --releasededup_monitor runs three workloads through the same cache — shared 4 KiB blobs, unique 24-byte values, and small values shared 2:1 — and prints what the monitor concludes about each.
Licensed under the MIT License.