Rustango docs
← Guides

Caching

Caching stores the result of expensive work — a heavy query, a rendered fragment, a third-party API call — so the next request gets it instantly instead of recomputing. Rustango gives you one Cache trait with swappable backends (in-memory, Redis, database), a compute-on-miss helper (get_or_set), and typed JSON helpers. Swap the backend without touching a single call site — like Django's cache framework or Laravel's Cache facade.

Caching in rustango: get_or_set checks the cache, runs the factory only on a miss, stores the result with a TTL, and serves hits instantly; the same Cache trait backs InMemory, Redis, and DB

New to a term here? cache, TTL, key, backend — see the glossary.

Source: rustango::cache (Cache, InMemoryCache, NullCache, get_or_set, get_json, set_json, BoxedCache, from_settings) — behind the cache feature (on by default). RedisCache needs the cache-redis feature (off by default).

Runnable version: every snippet is copied from cache_doc.rs (cargo test -p rustango --test cache_doc); the database backend is dogfooded on SQLite by cache_db_backend_sqlite_live.rs.

Table of contents


Step 1 — Pick a backend

Every backend implements the same Cache trait, so your code is identical whichever you choose. App code holds a BoxedCache (Arc<dyn Cache>) and never names the concrete type:

use rustango::cache::{BoxedCache, InMemoryCache};
use std::sync::Arc;

let cache: BoxedCache = Arc::new(InMemoryCache::new());
BackendFeatureUse for
InMemoryCachecachedev, tests, single process (per-process HashMap + TTL)
RedisCachecache-redisproduction; shared across replicas
DbCachecacheproduction without Redis; a rustango_cache table
NullCachecachedisable caching (every read misses) — handy in tests

Step 2 — get / set / delete

The core of the trait is four async methods. set takes an optional TTL (None = no expiry); get returns Option<String> (None on a miss):

use rustango::cache::{Cache, InMemoryCache};

let cache = InMemoryCache::new();

assert_eq!(cache.get("greeting").await?, None);     // miss
cache.set("greeting", "hello", None).await?;        // store, no expiry
assert_eq!(cache.get("greeting").await?.as_deref(), Some("hello"));
assert!(cache.exists("greeting").await?);

cache.delete("greeting").await?;                    // gone

There are batch variants too — get_many / set_many / delete_many.


Step 3 — get_or_set (cache-aside)

This is the one you'll reach for most. get_or_set returns the cached value, or — on a miss — runs your factory, stores the result with a TTL, and returns it. The factory only runs on a miss:

use rustango::cache::get_or_set;
use std::time::Duration;

let stats: HomeStats = get_or_set(
    &*cache,                       // &dyn Cache
    "home:stats",
    || async { compute_home_stats(&pool).await },   // runs only on a miss
    Some(Duration::from_secs(60)),                   // cache for 60s
).await?;

The backing test calls get_or_set twice for the same key and asserts the factory ran exactly once — the second call is served from the cache.

Invalidate on write. Cache-aside means stale data until the TTL expires. For data that changes, also delete(key) when you write it — e.g. from a post_save signal — so the next read recomputes.


Typed JSON values

get_json / set_json serialize any Serialize/Deserialize type to JSON, so you cache structs and lists, not just strings:

use rustango::cache::{get_json, set_json};

#[derive(serde::Serialize, serde::Deserialize)]
struct Profile { id: i64, name: String }

set_json(&*cache, "profile:7", &profile, None).await?;
let back: Option<Profile> = get_json(&*cache, "profile:7").await?;   // None on a miss

(get_or_set uses these under the hood, which is why its value type must be Serialize + Deserialize.)


TTL and expiry

Pass a Duration to set (or get_or_set) and the entry disappears after it. Verified: a 50 ms entry is readable immediately and gone after 80 ms.

cache.set("flash", "x", Some(Duration::from_millis(50))).await?;
// ...50ms later...
assert_eq!(cache.get("flash").await?, None);   // expired

InMemoryCache::with_default_ttl(d) sets a default TTL applied when you pass None.


Swapping backends

Because everything is the Cache trait, switching from in-memory to Redis is a one-line change at startup — usually driven by config so it differs per environment:

// Build the cache from `[cache]` settings (backend = "memory" | "redis" | "db" | "null").
let cache: BoxedCache = rustango::cache::from_settings(&settings.cache);

In production, point it at Redis (shared across all your replicas):

use rustango::cache::RedisCache;   // needs the `cache-redis` feature
let cache: BoxedCache = std::sync::Arc::new(RedisCache::new("redis://localhost").await?);

Same get / set / get_or_set calls — only the constructor changed.


Reference

Cache trait: get · set(key, value, ttl) · delete · exists · get_many / set_many / delete_many · get_or(key, default).

Free helpers: get_or_set(cache, key, factory, ttl) · get_json / set_json · from_settings(&CacheSettings).

What's built on the cache: server-side sessions, distributed rate limiting (CacheRateLimitLayer), idempotency keys, and feature flags all take a BoxedCache — so one Redis instance backs all of them.


See also

  • Background jobs — the other half of keeping requests fast (defer work instead of caching its result).
  • Sessions — a server-side store built on Cache.
  • MiddlewareCacheRateLimitLayer shares a counter across replicas via the cache.
  • ORM cookbook — invalidate cached reads from a post_save signal.