Skip to content
loco
v1.0
★ 6.9k Get started

Use the cache

Goal: cache a value (a computed result, an external API response, a hot query) behind a key, with an optional TTL, using a driver you can swap per environment.

ctx.cache is available in every controller, task, and worker. Values are serialized as JSON strings under the hood, so anything Serialize + DeserializeOwned can go in.

1. Configure a driver

Cache drivers are configured entirely in YAML — no code changes needed to switch drivers between environments.

# config/development.yaml — fast, disposable, no external dependency
cache:
kind: InMem
max_capacity: 33554432 # optional, bytes; default 32MiB (32 * 1024 * 1024)
# config/production.yaml — shared across processes
cache:
kind: Redis
uri: "{{ get_env(name='REDIS_CACHE_URL', default='redis://127.0.0.1:6379') }}"
max_size: 10 # required — max pool connections
# omit the `cache` key entirely, or set explicitly — this is the default
cache:
kind: Null

InMem needs the cache_inmem feature (on by default); Redis needs cache_redis (off by default — add it to your Cargo.toml). See the feature flags reference.

If you omit cache from the config file altogether, Loco silently falls back to the Null driver: get() always returns None, and every mutating operation (insert, insert_with_expiry, remove, clear, ping) returns an error. This is a fail-fast default for “you haven’t configured a real cache” — don’t ship it to production by accident.

2. Insert and read values

use loco_rs::cache;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}
async fn cache_basics(ctx: &AppContext) -> Result<()> {
ctx.cache.insert("greeting", &"hello".to_string()).await?;
let user = User { name: "Alice".to_string(), age: 30 };
ctx.cache.insert("user:1", &user).await?;
let greeting: Option<String> = ctx.cache.get("greeting").await?;
let cached_user: Option<User> = ctx.cache.get("user:1").await?;
let exists: bool = ctx.cache.contains_key("user:1").await?;
ctx.cache.remove("greeting").await?;
Ok(())
}

3. Set a TTL with insert_with_expiry

use std::time::Duration;
ctx.cache
.insert_with_expiry("session:abc", &token, Duration::from_secs(300))
.await?;

4. Cache the result of a computation with get_or_insert

get_or_insert returns the cached value if present, otherwise runs the given future, stores the result, and returns it. get_or_insert_with_expiry does the same but attaches a TTL to the freshly-computed value.

let expensive_report = ctx
.cache
.get_or_insert::<Report, _>("report:daily", async {
build_daily_report(ctx).await
})
.await?;
use std::time::Duration;
let expensive_report = ctx
.cache
.get_or_insert_with_expiry::<Report, _>(
"report:daily",
Duration::from_secs(3600),
async { build_daily_report(ctx).await },
)
.await?;

5. Health-check and clear

// Fails if the backing store (e.g. Redis) is unreachable.
ctx.cache.ping().await?;
// Wipe the cache.
ctx.cache.clear().await?;

Redis caveat: Cache::clear() on the Redis driver issues FLUSHDB — it flushes the entire Redis logical database, not just the keys your app put there. If other data (session store, queue, another app) shares that same Redis DB/instance, clear() will delete it too. Point cache at its own Redis DB (redis://host:6379/1, a separate db index) if you need to isolate it, and treat clear() as a blunt, whole-database operation.

6. Verify

#[tokio::test]
async fn can_get_or_insert() {
let app_ctx = get_app_context().await; // your test AppContext
let key = "loco";
assert_eq!(app_ctx.cache.get::<String>(key).await.unwrap(), None);
let result = app_ctx
.cache
.get_or_insert::<String, _>(key, async { Ok("loco-cache-value".to_string()) })
.await
.unwrap();
assert_eq!(result, "loco-cache-value");
assert_eq!(
app_ctx.cache.get::<String>(key).await.unwrap(),
Some("loco-cache-value".to_string())
);
}

Reference