Rust cache structures and easy function memoization
Caching structures and simplified function memoization
cached provides implementations of several caching structures as well as macros
for defining memoized functions.
When using the store types directly, start with use cached::prelude::*;. The short
method names (get/set/len/...) live on blanket extension traits, and the prelude
brings every trait in with one import (see Method naming below).
Requires Rust >= 1.92. (The async_core / async features do not compile before 1.92: the
CachedGetOrSetAsync default bodies hit a borrowck limitation rustc attributes to
rust-lang/rust#100013. rust-version is a
single crate-level value, so the floor applies to every feature set.)
Memoized functions defined using #[cached]/#[once] macros are thread-safe with the backing
function-cache wrapped in a mutex/rwlock. #[concurrent_cached] functions are thread-safe via the
store's own internal synchronization: sharded stores use per-shard parking_lot::RwLock; Redis and
disk stores rely on their respective server/file-system concurrency.
By default, #[cached] uses no write synchronization: concurrent uncached calls for the same key may
each compute independently and overwrite each other, matching the 2.x behavior and Python's
functools.lru_cache. Set sync_writes = "by_key" to deduplicate concurrent first calls for the same
key through bucketed per-key locks. Set sync_writes = true (or "default") to hold the whole-cache
lock for the duration of each miss. Note: "by_key" holds the per-key bucket lock across the entire
function body, so it must not be used on recursive or re-entrant memoized functions (deadlock risk when
keys in the active call chain share a bucket). #[once] defaults to no synchronization (add
sync_writes = true to serialize concurrent first-calls); #[concurrent_cached] does not support
sync_writes. The number of per-key lock buckets for "by_key" is tunable with
sync_writes_buckets = N (default 64).
cached::stores docs for the available cache stores.macros docs for more macro examples.Upgrading from 2.x? See the migration guide for a step-by-step walkthrough, or the agent-oriented guide for the complete breaking-change list.
Upgrading from 1.x? 2.0 contains breaking changes (new
cache_remove_entryrequired method,Result/Optioncaching behavior flipped to smart-by-default,result/optionattributes removed, and more). See the 2.0 migration guide for a step-by-step walkthrough.Upgrading from a pre-1.0 release? 1.0 contains breaking changes (store renames, removed declarative macros, renamed macro/builder attributes, and a changed Redis key format). See the 1.0 migration guide for a step-by-step walkthrough, or the agent-oriented guide for automated migration tooling.
Method naming
Every synchronous cache operation has a short alias (get/set/remove/clear/len/...) and a
cache_-prefixed form (cache_get/cache_set/cache_remove/cache_clear/cache_size/...).
The short aliases are the preferred spelling. Use the cache_-prefixed names when a short alias
would collide with another in-scope trait's method of the same name (for example, your type also
implements a trait with its own get).
The get/set/remove short aliases for Cached stores live on CachedExt; those for
ConcurrentCached stores live on ConcurrentCachedExt. Both extension traits have blanket
implementations, so the short names are always available when the extension trait is in scope.
The simplest way to get them is use cached::prelude::*;, which re-exports both extension traits.
Alternatively, import them directly: use cached::{Cached, CachedExt};. Custom store
implementations only need to implement the cache_-prefixed required methods on the core trait;
the short aliases come for free via the blanket extension trait impl.
For Cached stores, len/is_empty are also on CachedExt. For ConcurrentCached stores,
size introspection is cache_size and cache_is_empty on ConcurrentCacheBase (the shared base
trait), not on ConcurrentCachedExt: bring ConcurrentCacheBase into scope to call them on a
generic bound. Note that ConcurrentCacheBase::cache_size returns Result<Option<usize>, _>:
fallible for IO-backed stores, and None when the backend cannot report an exact count. The sharded
stores keep their inherent infallible len/is_empty too, which take priority at the call site.
Both async traits use the async_cache_* spelling. ConcurrentCachedAsync mirrors the sync
ConcurrentCached surface (async_cache_get, async_cache_set, async_cache_remove, ...) for
concurrent stores that manage their own synchronization (the in-memory sharded stores, plus the
IO-backed redis and redb stores). CachedGetOrSetAsync is narrower: it
only memoizes an async closure over a synchronous in-memory Cached store, via the
async_cache_get_or_set_with family (async_cache_get_or_set_with,
async_cache_try_get_or_set_with, and their _mut variants).
ConcurrentCachedAsync has its own deduplicated aliases on ConcurrentCachedAsyncExt
(async_get, async_set, async_remove, async_remove_entry, async_delete,
async_contains, async_clear, async_reset, async_get_or_set_with,
async_try_get_or_set_with). They keep the async_ prefix instead of being bare get/set,
because the stores implementing ConcurrentCachedAsync also implement the synchronous
ConcurrentCached: bare names would be a second applicable candidate alongside
ConcurrentCachedExt and make store.get(&k) ambiguous. Size and metric introspection is not
aliased there; it is cache_size / cache_is_empty and the metric readers on
ConcurrentCacheBase, which are callable on an async store with no extension trait imported.
CachedGetOrSetAsync has no alias trait; its async_ prefix already prevents collisions with
the sync methods.
Features
default: Include proc_macro, ahash, and time_stores featuresproc_macro: Include proc macrosahash: Enable the optional ahash hasher as default hashing algorithm.async_core: Async trait definitions (the runtime-agnostic async cache traits) without the async-lock dependency. Enabled by async.async: Include support for async functions and async cache stores (runtime-agnostic; no tokio dependency; uses async-lock)redis_store: Include Redis cache storeredis_smol: Include async Redis support using smol (no TLS); implies redis_store and asyncredis_smol_native_tls: redis_smol + TLS via native-tls (system TLS library)redis_smol_rustls: redis_smol + TLS via rustls (pure-Rust TLS)redis_tokio: Include async Redis support using tokio (no TLS); implies redis_store and asyncredis_tokio_native_tls: redis_tokio + TLS via native-tls (system TLS library)redis_tokio_rustls: redis_tokio + TLS via rustls (pure-Rust TLS)redis_connection_manager: Enable the optional connection-manager capability of redis. Additive: async redis
caches keep using a MultiplexedConnection by default; opt a specific cache into the auto-reconnecting connection
manager with .connection_manager(true) on its builder. This capability feature pulls in redis_store and async
itself, but it is runtime-agnostic (redis/connection-manager needs only redis/aio), so it carries no runtime:
pair it with a runtime feature (redis_tokio* or redis_smol*) to actually connect; enabling it alone leaves you
without a runtime. Does not enable TLS.redis_async_cache: Enable Redis client-side caching over RESP3 for async Redis caches.
Implies async and redis_store, but is runtime-agnostic (redis/cache-aio needs only redis/aio): pair it with a
runtime feature (redis_tokio* or redis_smol*) or the build has no runtime to connect with. Does not enable TLS.redb_store: Include disk cache storetime_stores: Include time-based cache stores (TtlCache, LruTtlCache, TtlSortedCache, ShardedTtlCache, and ShardedLruTtlCache).
Also required when using #[cached(ttl_secs = ...)], #[cached(ttl = ...)], #[cached(ttl_millis = ...)], #[concurrent_cached(ttl_secs = ...)], #[concurrent_cached(ttl = ...)], or #[concurrent_cached(ttl_millis = ...)] on the default in-memory path. (#[once] has its own ungated timer, so #[once(ttl_secs = ...)] does NOT require this feature.)
Disable this feature when targeting environments without system time support (e.g. wasm32-unknown-unknown without WASI or JS).The procedural macros (#[cached], #[once], #[concurrent_cached]) offer a number of features, including async support.
See the macros module for more samples, and the
examples directory for runnable snippets.
Any custom cache that implements cached::Cached can be used with the #[cached]/#[once] macros in place of the built-ins (cached::CachedGetOrSetAsync additionally memoizes an async closure over such a store).
Any custom cache that implements cached::ConcurrentCached/cached::ConcurrentCachedAsync can be used with the #[concurrent_cached] macro.
Macro quick reference
| Use case | Annotated signature |
|---|---|
#[cached] |
|
| Unbounded memoize (default; concurrent misses each compute independently) | #[cached] fn fib(n: u64) -> u64 |
| Unbounded memoize, explicit no-sync (same as default) | #[cached(sync_writes = false)] fn fib(n: u64) -> u64 |
| LRU-bounded — evict past N entries | #[cached(max_size = 1_000)] fn lookup(id: u32) -> Row |
| TTL — expire results after N whole seconds | #[cached(ttl_secs = 60)] fn config() -> Config |
TTL as a Duration expression (inlined verbatim, so Duration must be in scope; see note below) |
#[cached(ttl = "Duration::from_secs(60)")] fn config() -> Config |
| TTL in milliseconds (sub-second capable; Redis honors millisecond TTL via PSETEX/PEXPIRE) | #[cached(ttl_millis = 500)] fn poll(id: u64) -> Status |
| LRU + TTL | #[cached(max_size = 500, ttl_secs = 300)] fn search(q: String) -> Vec<Hit> |
Don't cache None returns (implicit for Option<T>) |
#[cached] fn find(id: u64) -> Option<User> |
Don't cache Err returns (implicit for Result<T, E>) |
#[cached] fn load(id: u64) -> Result<Data, E> |
Force-cache None returns |
#[cached(cache_none = true)] fn find(id: u64) -> Option<User> |
Force-cache Err returns |
#[cached(cache_err = true)] fn load(id: u64) -> Result<Data, E> |
Serve stale value when function returns Err |
#[cached(result_fallback = true, ttl_secs = 60)] fn fetch(id: u64) -> Result<Data, E> |
| Per-value / dynamic per-entry TTL (value carries its own expiry) | #[cached(expires = true)] fn token(scope: String) -> Token |
| Deduplicate concurrent first calls per key (opt-in; do not use on recursive functions) | `#[cached(ttl_secs = 30, sync_writes = "by_key")] fn expensive(id: |
No open issues yet, or sync has not completed.