Docs
Data caching
Deduplicate loads, cache data across requests, and invalidate render dependencies.
Proa keeps data cache policy explicit. The framework does not silently cache data fetches for you. Choose the boundary that matches the work:
| Resource | Cache at |
|---|---|
| Request-local data | DataResolver |
| Cross-request data | application loader, StandardDataLoader, Redis, database, or origin service |
For complete HTML responses, HTTP cache headers, ETags, and Cloudflare setup, see Page caching.
Cache Boundaries
Request-local dedupe prevents components in one render from loading the same key twice. Cross-request caching reduces origin reads across users or requests. Prefer the narrowest cache that removes real work without sharing user-specific data.
Data caching reduces work when a request reaches the application. A fresh CDN page-cache hit skips the application entirely. Each cache needs its own freshness and invalidation policy.
Request-Local Dedupe
Wrap your loader with DataResolver when multiple components can ask for the same key during one render:
use std::sync::Arc;
use proa_stream::DataResolver;
pub async fn product_handler() -> impl axum::response::IntoResponse {
let app_loader = Arc::new(AppLoader::new());
let loader = DataResolver::request_scope(app_loader);
RouteResponse::ssr_async_with_loader(ProductPage { id: "sku_1" }, loader)
}
This is a per-render optimization. It does not persist values across requests.
Cross-Request Data Cache
For a process-local cache, wrap an upstream loader with StandardDataLoader:
use std::sync::Arc;
use std::time::Duration;
use proa_core::StandardDataLoader;
let cached_loader = Arc::new(StandardDataLoader::new(
AppLoader::new(),
Duration::from_secs(30),
));
StandardDataLoader stores LoadValue by LoadKey, expires entries by TTL, and exposes manual invalidation:
use proa_core::LoadKey;
cached_loader.invalidate(&LoadKey::named("product", "sku_1"));
cached_loader.clear();
Use a process-local cache only when each instance can safely have its own copy. For horizontally scaled apps, put shared cache state in Redis, your database, your CDN, or the upstream service.
Load Hints
LoadHints carry cache policy without changing the identity of the data:
use std::time::Duration;
use proa_core::{CacheHint, CacheMode, LoadHints, LoadKey};
let product = cx
.load_json_with::<Product>(
LoadKey::named("product", "sku_1"),
LoadHints::new().with_cache(
CacheHint::ttl(Duration::from_secs(60)).with_mode(CacheMode::Refresh),
),
)
.await?;
The built-in cache modes are:
| Mode | Meaning |
|---|---|
UseLoaderDefault | Let the loader apply its normal policy. |
Bypass | Skip the loader-managed cache and fetch from origin. |
Refresh | Fetch from origin and update the cache entry. |
OnlyIfCached | Return cached data or fail with LoadError::CacheMiss. |
Custom loaders may honor hints, ignore them, or map them onto their own cache system. Keep LoadKey stable; put freshness policy in LoadHints.
Invalidation
Choose invalidation at the same boundary as the cache:
| Cache | Invalidate by |
|---|---|
DataResolver | end of request |
StandardDataLoader | TTL, invalidate(&key), or clear() |
| Redis/database cache | app-specific key, tag, or write transaction |
When data changes through a form or API mutation, invalidate the data cache before redirecting or returning the success response.
If that data appears in cached HTML, invalidate the affected CDN pages too. Clearing a loader cache does not remove an already rendered edge response. See Page caching.
pub async fn update_product(Form(form): Form<ProductForm>) -> Redirect {
save_product(&form).await;
cached_loader.invalidate(&LoadKey::named("product", form.id.as_str()));
Redirect::to("/products")
}
Cache Trace
WebContext exposes cache_trace_mut() for render-time dependency recording:
cx.cache_trace_mut().set_ttl(Duration::from_secs(60));
cx.cache_trace_mut().depend_tag("product:sku_1");
cx.cache_trace_mut().depend_path("/products/sku_1");
Use this when your application wants to collect dependencies during render and map them to its own invalidation system. Proa records the trace; your app decides how to consume it. Recording a TTL or tag here does not automatically emit CDN headers or call Cloudflare's purge API.
Production Checklist
- Wrap loaders with
DataResolverwhen a render can load the same key repeatedly. - Use
StandardDataLoaderonly when process-local cache state is acceptable. - Put cross-instance cache state in shared infrastructure.
- Include the relevant user or tenant identity when caching private data across requests.
- Invalidate on mutation before redirecting.
- Invalidate any affected CDN pages separately.
See Data loading and streaming, Route handlers, and Page caching.