ax-cache
A concurrent in-memory cache for Rust with admission-controlled eviction.
What it is
- Sharded
hashbrownmap with oneparking_lot::RwLockper shard. - S3-FIFO eviction (small-queue / main-queue split with frequency-based promotion).
- TinyLFU admission (Count-Min Sketch) keeps one-hit-wonders from displacing established hot keys.
- AES-NI accelerated hashing via
axhashwhen the CPU supports it; portable fallback otherwise.
Install
[]
= "0.1"
No feature flags. AES detection is automatic at runtime.
API at a glance
use ;
use Duration;
// Construction
let cache: = new; // auto-shard
let cache: = with_shards; // explicit
// Reads — accept any borrowed form (e.g. `&str` for `String` keys, no allocation).
let value: = cache.get;
let exists: bool = cache.contains_key;
let removed: = cache.remove;
// Writes — return InsertOutcome (Inserted / Updated / Rejected).
let r: InsertOutcome = cache.insert;
let r: InsertOutcome = cache.insert_with_ttl;
assert!; // == Inserted | Updated
// Bulk
cache.clear;
let n: usize = cache.len;
let empty: bool = cache.is_empty;
// Observability
let m = cache.metrics; // hits, misses, insertions, evictions, rejections
InsertOutcome makes write semantics explicit:
| Variant | Meaning | is_present() |
|---|---|---|
Inserted |
Key was new and admitted into the cache. | true |
Updated |
Key already existed; value (and TTL) was replaced. | true |
Rejected |
Cache was full; admission filter declined this key. | false |
Examples
Basic
use Cache;
let cache: = new;
cache.insert;
if let Some = cache.get
TTL
use Cache;
use Duration;
let cache: = new;
cache.insert_with_ttl;
assert!;
Expired entries are reclaimed lazily on access. To reclaim them proactively in the background, opt into the maintenance thread:
use ;
use Duration;
let cache: = new;
cache.enable_maintenance;
The maintenance thread stops automatically when the cache is dropped.
Shared across threads
use Cache;
use Arc;
use thread;
let cache = new;
let handles: = .map.collect;
for h in handles
Metrics
use Cache;
let cache: = new;
// ... use cache ...
let m = cache.metrics;
let total = m.hits + m.misses;
if total > 0
println!;
Performance
The numbers below come from running the benchmarks in this repository on
an Apple Silicon laptop. They are reproducible — run cargo bench to
verify on your machine. Performance varies with workload, hardware,
and concurrency level; treat these as ballpark figures, not
guarantees.
Single-thread microbench (uncontended)
cargo bench --bench single_thread (Cache<u64, u64>):
| Operation | Latency | Throughput |
|---|---|---|
get_hit |
~8 ns | ~120 Mops/s |
get_miss |
~14 ns | ~70 Mops/s |
insert_update |
~12 ns | ~85 Mops/s |
insert_new |
~70 ns | ~14 Mops/s |
insert_new is dominated by allocation, hashing, queue admission, and
(on overflow) eviction work; insert_update only mutates an existing
slot.
Contention sweep (Zipfian, write-heavy)
cargo bench --bench contention with cap=1M, universe=10M, zipf α=0.99, 5% writes:
| Threads | Throughput | P50 | P99 | Hit ratio |
|---|---|---|---|---|
| 1 | ~9 Mops/s | ~42 ns | ~250 ns | ~0.83 |
| 4 | ~30 Mops/s | ~83 ns | ~290 ns | ~0.84 |
| 8 | ~35 Mops/s | ~125 ns | ~420 ns | ~0.84 |
30-minute soak (realistic workload)
8 threads, Cache<u64, Arc<[u8]>> with 256-byte payloads, cap=1M,
universe=10M, zipf α=0.99, 5% writes:
| Metric | Value |
|---|---|
| Throughput | 11.4 Mops/s |
| Hit ratio | 0.84 |
| P50 | 500 ns |
| P99 | 29 µs |
| P999 | 101 µs |
| Peak RSS | ~150 MB |
| Final entries | 1,000,000 |
The gap between the contention bench (u64 values, in-memory tight
loop) and the soak test (Arc<[u8]> payloads, longer run) reflects
DRAM bandwidth and OS scheduler effects on long-running workloads. P99+
spikes in the soak test are dominated by OS scheduling, not cache code.
Hit ratio vs Zipfian skew
| α (skew) | Hit ratio |
|---|---|
| 0.99 (typical) | ~0.84 |
| 1.2 (heavy) | ~0.96 |
Hit ratio scales with workload skew: the more concentrated the access pattern on a few hot keys, the more effectively S3-FIFO + TinyLFU preserves them.
Scan resistance
cargo bench --bench scan_resistance (cap=10k, 1k hot keys + 100k
scan keys): hot-key survival rate is ~90–100% depending on
admission pressure. Cold scan keys are dropped from the small queue
within a handful of evictions; hot keys live in the main queue.
When to use ax-cache
Good fit:
- Server-side hot-data caches with concentrated access patterns (Zipfian, power-law).
- Workloads that need bounded memory with predictable eviction.
- Scenarios where occasional one-hit-wonder reads should not pollute the cache (admission filtering helps).
Less good fit:
- Caches where every key matters equally and rejection is unacceptable
(consider a plain
RwLock<HashMap>ordashmap). - Persistent / off-heap caches (this is in-memory only).
- Workloads dominated by very large values where memory bandwidth is the bottleneck rather than hash table operations.
Limitations / honest caveats
capacityis a soft target. The cache holds approximatelycapacityentries in steady state; it may briefly sit a few entries above capacity between inserts.- No async API. All operations are synchronous and lock-based.
- Values are cloned on
get. IfVis expensive to clone, wrap it inArc<T>. - Tail latency at high concurrency is OS-bound on the slowest percentiles. P99 spikes in long-running workloads correlate with scheduler events (preemption, page faults), not with cache internals.
- No serialization / persistence. Drop the cache, lose the data.
License
MIT — see LICENSE.