Skip to content
Projects

jaemk/cached has 2,097 GitHub stars and 109 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-07 23:08 UTC. Last push 2026-11-01.

How these counts are measured

Rust cache structures and easy function memoization

Measured star growth

in 24h
0
in 7 days
Update delayed
in 15 days
Update delayed
in 1 month
Update delayed

Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.

README.md

Original

cached

Build Status crates.io docs

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 cache

The 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"

Contents

Memoizing functions

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 #[cached] macro

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);

The #[once] macro

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
}

The #[concurrent_cached] macro

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)
}

Common attributes

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.

Result and Option returns

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.

Async functions

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
}

Redis and disk caches

#[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.

Using cache stores directly

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();

Choosing a store

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.

Per-value expiry

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 flags

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

Requirements

  • Rust 1.92 or later.
  • Arguments must be Hash + Eq + Clone, or supply key and convert to build a key that is. Floats are the common case that needs convert, e.g. convert = { format!("{x:.6}") }.
  • Return values must be Clone; they are cloned on every hit. Return Arc<T> when T is expensive to clone.
  • Cached functions should not have side effects, since a hit skips the body.
  • Generic functions need key and convert to produce a concrete key type.

Further reading

Upgrading

Migration guides for each release are in docs/migrations. The major ones:

License: MIT

Explore related projects