salvo-caching
Salvo Caching Strategies
[dependencies]
salvo = { version = "0.89.3", features = ["cache", "caching-headers"] }
The cache feature activates salvo-cache with the default moka-store backend.
Response cache middleware
salvo::cache::Cache caches full responses (status, headers, body). By default it caches GET only and skips streaming responses and error bodies.
use std::time::Duration;
use salvo::cache::{Cache, MokaStore, RequestIssuer};
use salvo::prelude::*;
#[handler]
async fn expensive() -> String {
format!("computed at {}", chrono::Utc::now())
}
#[tokio::main]
async fn main() {
let cache = Cache::new(
MokaStore::builder()
.time_to_live(Duration::from_secs(60))
.max_capacity(10_000)
.build(),
RequestIssuer::default(),
);
let router = Router::new().hoop(cache).get(expensive);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Cache::new(store, issuer) is the only constructor. Tunable:
.skipper(impl Skipper)— default isMethodSkipper::new().skip_all().skip_get(false)(GET only). Pass a closure|req, depot| boolto override.
RequestIssuer
Builds cache keys from the request URI and method. Toggle parts:
let issuer = RequestIssuer::new()
.use_scheme(false)
.use_authority(false)
.use_path(true)
.use_query(true)
.use_method(true);
All five are true by default.
Custom CacheIssuer
Implement CacheIssuer (or pass a closure) to vary cache keys by user, tenant, etc.:
use salvo::cache::CacheIssuer;
let issuer = |req: &mut Request, depot: &Depot| -> Option<String> {
let user_id = depot.get::<String>("user_id").ok()?;
Some(format!("{user_id}:{}", req.uri().path()))
};
let cache = Cache::new(store, issuer);
Returning None disables caching for that request.
CachingHeaders (ETag + Last-Modified)
salvo::caching_headers::CachingHeaders adds ETag and Last-Modified handling to downstream handlers, responding 304 when If-None-Match / If-Modified-Since match:
use salvo::caching_headers::CachingHeaders;
let router = Router::new()
.hoop(CachingHeaders::new())
.get(handler);
For ETag-only or Last-Modified-only, use ETag::new() or Modified::new() from the same module.
HTTP cache headers manually
#[handler]
async fn cached(res: &mut Response) -> &'static str {
res.headers_mut().insert(
"cache-control",
"public, max-age=3600, stale-while-revalidate=86400".parse().unwrap(),
);
"hello"
}
Directives cheat sheet:
public, max-age=N— shared caches may store for N secondsprivate, max-age=N— browser onlyno-store— never cacheno-cache— must revalidate with originstale-while-revalidate=N— serve stale for N seconds while revalidating
Manual 304 response
StatusError has no not_modified(). Set the status directly:
#[handler]
async fn with_etag(req: &mut Request, res: &mut Response) {
let etag = compute_etag();
if req.header::<String>("if-none-match").as_deref() == Some(etag.as_str()) {
res.status_code(StatusCode::NOT_MODIFIED);
return;
}
res.headers_mut().insert("etag", etag.parse().unwrap());
res.render(Json(load_data().await));
}
Data-layer caching with Moka
For caching values inside handlers (not full responses), use moka::future::Cache directly and share via affix_state:
moka = { version = "0.12", features = ["future"] }
use moka::future::Cache as MokaCache;
use std::sync::Arc;
type UserCache = Arc<MokaCache<i64, User>>;
#[handler]
async fn get_user(req: &mut Request, depot: &mut Depot) -> Result<Json<User>, StatusError> {
let id = req.param::<i64>("id").ok_or_else(StatusError::bad_request)?;
let cache = depot.obtain::<UserCache>().unwrap();
let pool = depot.obtain::<PgPool>().unwrap();
if let Some(user) = cache.get(&id).await { return Ok(Json(user)); }
let user = sqlx::query_as::<_, User>("select id, name from users where id = $1")
.bind(id).fetch_optional(pool).await
.map_err(|_| StatusError::internal_server_error())?
.ok_or_else(StatusError::not_found)?;
cache.insert(id, user.clone()).await;
Ok(Json(user))
}
#[tokio::main]
async fn main() {
let cache: UserCache = Arc::new(
MokaCache::builder()
.max_capacity(1_000)
.time_to_live(std::time::Duration::from_secs(60))
.build(),
);
let router = Router::new()
.hoop(affix_state::inject(cache))
.push(Router::with_path("users/{id}").get(get_user));
// ...
}
Invalidate on write:
#[handler]
async fn update_user(req: &mut Request, depot: &mut Depot) -> StatusCode {
let id = req.param::<i64>("id").unwrap();
// update db...
depot.obtain::<UserCache>().unwrap().invalidate(&id).await;
StatusCode::OK
}
invalidate_all() clears everything; iterate over IDs for bulk invalidation.
Gotchas
Cacheis full-response. It needs a deterministic key — beware caching per-user data under a sharedRequestIssuer; use a custom issuer keyed on user ID.- Streaming responses (
ResBody::Stream) and error bodies are never cached, silently. Cache::newdoes not return a builder — configure the store viaMokaStore::builder().- Method names:
MokaStore::builder()(notCache::builder),RequestIssuer::default()ornew(). StatusError::not_modified()does NOT exist — setStatusCode::NOT_MODIFIEDdirectly.
Related Skills
- salvo-compression: Compress before
Cacheso the stored body is already gzipped. - salvo-static-files:
serve-staticsets ETag/Last-Modified automatically. - salvo-database: Cache hot query results at the data layer.
More from salvo-rs/salvo-skills
salvo-csrf
Implement CSRF (Cross-Site Request Forgery) protection using cookie or session storage. Use for protecting forms and state-changing endpoints.
16salvo-auth
Implement authentication and authorization using JWT, Basic Auth, or custom schemes. Use for securing API endpoints and user management.
15salvo-cors
Configure Cross-Origin Resource Sharing (CORS) and security headers. Use for APIs accessed from browsers on different domains.
15salvo-proxy
Implement reverse proxy to forward requests to backend services. Use for load balancing, API gateways, and microservices routing.
15salvo-openapi
Generate OpenAPI documentation automatically from Salvo handlers. Use for API documentation, Swagger UI, and API client generation.
15salvo-path-syntax
Path parameter syntax guide for Salvo routing. Explains the `{}` syntax (v0.76+) vs deprecated `<>` syntax, with migration examples.
14