Caching Middleware¶
Cello provides smart caching middleware implemented in Rust. It caches HTTP responses in memory with configurable TTL, tag-based invalidation, and ETag support for conditional requests.
Quick Start¶
How It Works¶
Request arrives
│
â–¼
Is method cacheable? (GET/HEAD)
│
Yes│ No
â–¼ â–¼
Cache Skip to handler
lookup
│
Hit│ Miss
â–¼ â–¼
Return Run handler
cached Cache response
response Return response
The middleware stores responses in a Rust DashMap (lock-free concurrent HashMap) keyed by the request method, path, and query string.
Configuration¶
app.enable_caching(
ttl=300, # Default TTL in seconds
methods=["GET", "HEAD"], # HTTP methods to cache
exclude_paths=["/api/realtime", "/ws"] # Paths to skip
)
| Parameter | Type | Default | Description |
|---|---|---|---|
ttl | int | 300 | Time-to-live in seconds |
methods | list[str] | ["GET", "HEAD"] | HTTP methods to cache |
exclude_paths | list[str] | [] | Paths excluded from caching |
Per-Route Caching with the @cache Decorator¶
Override the default TTL or add cache tags on individual routes. The @cache decorator supports both sync and async handlers -- it automatically detects the handler type and wraps it correctly, so async handlers are properly awaited.
from cello import App, cache
app = App()
app.enable_caching(ttl=300)
# Cache this route for 1 hour (sync handler)
@app.get("/api/config")
@cache(ttl=3600)
def get_config(request):
return {"theme": "dark", "language": "en"}
# Cache with tags for targeted invalidation (sync handler)
@app.get("/api/users")
@cache(ttl=600, tags=["users"])
def list_users(request):
return {"users": ["Alice", "Bob"]}
# Cache an async handler -- works identically
@app.get("/api/posts")
@cache(ttl=600, tags=["posts"])
async def list_posts(request):
posts = await fetch_posts_from_db()
return {"posts": posts}
The @cache decorator sets X-Cache-TTL and X-Cache-Tags headers that the Rust caching middleware reads. When applied to an async handler, the decorator awaits the coroutine before setting headers, so there is no risk of returning an unawaited coroutine.
Cache Invalidation¶
Tag-Based Invalidation¶
Invalidate all cached responses that share a tag:
@app.post("/api/users")
def create_user(request):
data = request.json()
# ... create user in database ...
# Invalidate all responses tagged with "users"
app.invalidate_cache(tags=["users"])
return Response.json({"id": 1, **data}, status=201)
Time-Based Expiration¶
Cached entries automatically expire after their TTL:
# Entries expire after 300 seconds (5 minutes)
app.enable_caching(ttl=300)
# Per-route: this entry expires after 60 seconds
@app.get("/api/stock-price")
@cache(ttl=60)
def stock_price(request):
return {"price": 142.50}
ETag Support¶
Cello automatically generates ETags for cached responses, enabling conditional requests:
# First request -- full response
GET /api/users HTTP/1.1
HTTP/1.1 200 OK
ETag: "a1b2c3d4"
Content-Type: application/json
{"users": [...]}
# Subsequent request -- conditional
GET /api/users HTTP/1.1
If-None-Match: "a1b2c3d4"
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4"
The 304 Not Modified response saves bandwidth by not re-sending the body.
Cache Headers¶
Cached responses include diagnostic headers:
| Header | Description |
|---|---|
X-Cache | HIT or MISS |
X-Cache-TTL | Remaining TTL in seconds |
ETag | Entity tag for conditional requests |
Cache-Control | Standard HTTP cache directives |
What Is Not Cached¶
The middleware skips caching for:
- Non-GET/HEAD methods (POST, PUT, DELETE, PATCH)
- Paths listed in
exclude_paths - Responses with
Cache-Control: no-storeorno-cache - Responses with status codes outside the 2xx range
Example: Full Caching Setup¶
from cello import App, cache
app = App()
app.enable_caching(
ttl=300,
exclude_paths=["/api/auth", "/health", "/metrics"]
)
# Short-lived cache for frequently changing data (async)
@app.get("/api/feed")
@cache(ttl=30, tags=["feed"])
async def feed(request):
posts = await get_recent_posts()
return {"posts": posts}
# Long-lived cache for static configuration (sync)
@app.get("/api/settings")
@cache(ttl=3600, tags=["settings"])
def settings(request):
return {"maintenance_mode": False}
# Invalidate on write
@app.post("/api/posts")
async def create_post(request):
data = request.json()
await save_post(data)
app.invalidate_cache(tags=["feed"])
return Response.json(data, status=201)
Performance¶
| Operation | Overhead |
|---|---|
| Cache lookup (hit) | ~100ns |
| Cache lookup (miss) | ~100ns |
| Cache store | ~200ns |
| ETag comparison | ~50ns |
| Tag invalidation | ~1us per tag |
Memory Management
Cached responses are stored in memory. For applications with many unique URLs, set a reasonable TTL and use exclude_paths to avoid caching large or dynamic responses.
Next Steps¶
- Middleware Overview - Full middleware system
- Circuit Breaker - Fault tolerance
- Compression - Compress cached responses
- Rate Limiting - Throttle requests