Expand description
Server-side response cache with request coalescing and stale-while-revalidate
ResponseCache keeps successful GET responses in memory so an expensive
handler (database aggregate, upstream call, rendered page) runs rarely:
- Coalescing: when many requests miss at once, one runs the handler and the others wait for its result, so a cold or expired entry cannot stampede the backend
- Stale-while-revalidate: for a window after expiry the old response is served immediately while a single background request refreshes it, so clients never wait for a refresh
- Safe by default: requests carrying
AuthorizationorCookiebypass the cache (or get their own entries withResponseCache::per_user), responses withSet-Cookie,Cache-Control: no-store/private, streams and oversized bodies are never stored
Responses get X-Cache: HIT | MISS | STALE | COALESCED and Age. The
lifetime comes from the response’s Cache-Control: max-age / s-maxage
when present, otherwise from ResponseCache::ttl.
use std::time::Duration;
use ullun::cache::ResponseCache;
use ullun::prelude::*;
let cache = ResponseCache::new()
.ttl(Duration::from_secs(30))
.stale_while_revalidate(Duration::from_secs(120))
.vary_by(["accept-language"]);
App::new()
.middleware(cache.clone()) // keep a handle to invalidate after writes
.get("/report", |_req: Request| async { Ok(Response::text("expensive")) })
.post("/report/refresh", {
let cache = cache.clone();
move |_req: Request| {
let cache = cache.clone();
async move {
cache.invalidate_prefix("/report");
Ok(Response::text("ok"))
}
}
})
.run("127.0.0.1:3000")
.await
.unwrap();The cache lives in the memory of one process. A request that is not cacheable (see above) is not coalesced either: each waiter then runs the handler itself.
Structs§
- Response
Cache - Cache middleware, see the module documentation. Cloning gives another handle to the same cache.