Memoize Rust functions with one attribute. In-memory, sharded, Redis, and disk caches.
use cached::macros::cached;
/// Cache up to 1,000 users for 60 seconds each.
/// `Err` results are not cached, so a failed request is retried on the next call.
#[cached(max_size = 1_000, ttl_secs = 60)]
fn get_user(id: u64) -> Result<User, ApiError> {
api::fetch_user(id) // slow network call
}
let user = get_user(1)?; // calls the API
let user = get_user(1)?; // returned from the cacheThe same attribute works on async fn (with the async feature), and #[concurrent_cached]
swaps in a sharded store, Redis, or an on-disk cache without changing the function.
[dependencies]
cached = "4"- Memoizing functions
- Redis and disk caches
- Using cache stores directly
- Choosing a store
- Per-value expiry
- Feature flags
- Requirements
- Further reading
- Upgrading
The macros cache a function's return value keyed by its arguments. On each call the cache is
checked first, and the function body only runs on a miss. Each macro also generates
{fn}_no_cache (calls the body directly) and {fn}_prime_cache (recomputes and stores a fresh
value).
The general-purpose macro. Unbounded by default; add max_size for LRU eviction and ttl_secs
for expiry.
use cached::macros::cached;
#[cached]
fn fib(n: u64) -> u64 {
if n == 0 || n == 1 {
return n;
}
fib(n - 1) + fib(n - 2)
}
assert_eq!(fib(80), 23_416_728_348_467_685);Caches a single value regardless of arguments, for global config or tokens that are refreshed periodically.
use cached::macros::once;
#[once(ttl_secs = 300)]
fn config() -> Config {
load_config() // re-run at most once every 5 minutes
}Uses a sharded store with per-shard locks instead of one lock around the whole cache, so concurrent callers on different keys do not contend. It also backs the Redis and disk caches.
use cached::macros::concurrent_cached;
#[concurrent_cached(max_size = 10_000)]
fn lookup(id: u64) -> Row {
query(id)
}| Attribute | Effect |
|---|---|
max_size = N |
LRU eviction past N entries |
ttl_secs = N / ttl_millis = N |
Expire entries after a fixed duration |
refresh = true |
Reset an entry's TTL on every hit |
expires = true |
Each value decides its own expiry (see Per-value expiry) |
key = "Type", convert = { expr } |
Build the cache key from the arguments yourself |
sync_writes = "by_key" |
Concurrent first calls for the same key run the body once |
result_fallback = true |
On Err, return the last cached Ok value |
name = "NAME" |
Name of the generated cache static (defaults to the function name in caps) |
in_impl = true |
Cache a method inside an impl block |
See the macros module for the full
attribute reference, including synchronization options and argument and return-type
requirements.
A function returning Result<T, E> caches only Ok values, and one returning Option<T> caches
only Some. Opt back in with cache_err = true or cache_none = true. Detection matches the
literal Result / Option type names, so a type alias is cached as a plain value.
Enable the async feature and annotate an async fn. The feature is runtime-agnostic and does
not depend on tokio.
use cached::macros::cached;
#[cached(max_size = 100, ttl_secs = 30)]
async fn get(url: String) -> Body {
fetch(&url).await
}#[concurrent_cached] can store results in Redis (redis_* features) or on disk with
redb (redb_store feature). Values are serialized, so they must
implement serde::Serialize and serde::DeserializeOwned, and the function must return
Result so store errors have somewhere to go.
use cached::macros::concurrent_cached;
/// Reads the connection string from `CACHED_REDIS_CONNECTION_STRING`.
#[concurrent_cached(redis = true, ttl_secs = 300, map_error = |e| MyError(format!("{e:?}")))]
async fn report(id: u64) -> Result<String, MyError> {
fetch_report(id).await
}use cached::macros::concurrent_cached;
/// Stored under the system cache directory unless `disk_dir` is set.
#[concurrent_cached(disk = true, map_error = |e| MyError(format!("{e:?}")))]
fn thumbnail(n: u64) -> Result<String, MyError> {
Ok(render(n))
}See examples/redis-async-tokio.rs
and examples/disk.rs for
complete programs, including explicit store construction with ty and create.
Every store the macros use is also a plain type. Import the store and the
prelude, which brings the cache
traits into scope:
use cached::LruCache;
use cached::prelude::*;
let mut cache: LruCache<String, u32> = LruCache::new(100);
cache.set("a".to_string(), 1);
assert_eq!(cache.get("a"), Some(&1));
assert_eq!(cache.len(), 1);Single-owner stores like LruCache take &mut self and need your own lock to share across
threads. The Sharded* stores lock internally and can be shared directly:
use cached::ShardedLruCache;
let cache: ShardedLruCache<String, u32> = ShardedLruCache::new(1_000);
cache.set("a".to_string(), 1);
assert_eq!(cache.get("a"), Some(1));Builders configure TTLs, eviction callbacks, and the rest:
use cached::LruTtlCache;
use cached::time::Duration;
let cache: LruTtlCache<u64, String> = LruTtlCache::builder()
.max_size(1_000)
.ttl(Duration::from_secs(60))
.on_evict(|key, _value| println!("evicted {key}"))
.build()
.unwrap();| Need | Single-owner | Concurrent |
|---|---|---|
| Unbounded | UnboundCache |
ShardedUnboundCache |
| Size limit (LRU) | LruCache |
ShardedLruCache |
| Fixed TTL | TtlCache |
ShardedTtlCache |
| Size limit + fixed TTL | LruTtlCache |
ShardedLruTtlCache |
| Per-value expiry | ExpiringCache |
ShardedExpiringCache |
| Size limit + per-value expiry | ExpiringLruCache |
ShardedExpiringLruCache |
| Shared across processes | RedisCache, AsyncRedisCache |
|
| Persisted to disk | RedbCache |
TtlSortedCache is a TTL
cache ordered by expiry with an optional size limit. Custom stores work with the macros by
implementing Cached (for
#[cached] / #[once]) or
ConcurrentCached (for
#[concurrent_cached]).
The stores module documents each
store's eviction, locking, and on_evict behavior in detail.
When each value knows its own lifetime (an OAuth token, an HTTP response with Cache-Control),
implement Expires and set
expires = true:
use cached::macros::cached;
use cached::Expires;
use cached::time::{Duration, Instant};
#[derive(Clone)]
struct Token {
value: String,
expires_at: Instant,
}
impl Expires for Token {
fn is_expired(&self) -> bool {
Instant::now() >= self.expires_at
}
}
#[cached(expires = true)]
fn token(scope: String) -> Token {
Token { value: format!("token-{scope}"), expires_at: Instant::now() + Duration::from_secs(3600) }
}See examples/expires_per_key.rs.
| Feature | Default | Enables |
|---|---|---|
proc_macro |
yes | #[cached], #[once], #[concurrent_cached] |
time_stores |
yes | TTL stores and the ttl_* macro attributes (disable for wasm32-unknown-unknown without time support) |
ahash |
yes | ahash as the default hasher |
async |
Async functions and async store traits (runtime-agnostic) | |
async_core |
Async traits only, without the async-lock dependency |
|
redis_store |
Synchronous Redis store | |
redis_tokio / redis_smol |
Async Redis on tokio or smol | |
redis_tokio_native_tls / redis_tokio_rustls |
redis_tokio with TLS |
|
redis_smol_native_tls / redis_smol_rustls |
redis_smol with TLS |
|
redis_connection_manager |
Auto-reconnecting Redis connections (pair with a runtime feature) | |
redis_async_cache |
Redis client-side caching over RESP3 (pair with a runtime feature) | |
redb_store |
Disk store backed by redb |
- Rust 1.92 or later.
- Arguments must be
Hash + Eq + Clone, or supplykeyandconvertto build a key that is. Floats are the common case that needsconvert, e.g.convert = { format!("{x:.6}") }. - Return values must be
Clone; they are cloned on every hit. ReturnArc<T>whenTis expensive to clone. - Cached functions should not have side effects, since a hit skips the body.
- Generic functions need
keyandconvertto produce a concrete key type.
macros: every attribute, synchronization options, and more examplesstores: store comparison, method naming, sharding, and behavioral guaranteesclaim: single-flight guard for background refreshesexamples/: runnable programs, including stale-while-revalidate and refresh-before-expiry
Migration guides for each release are in
docs/migrations. The major
ones:
License: MIT