Cache Warmup Strategies
The first request to a cold cache pays the full cost: database queries, API calls, computation. If that first request is a user-facing page, they get a slow response.
Warmup means pre-populating the cache with known data before any user traffic arrives. The first request hits the cache instead of the database.
Simple per-function warmup
The simplest approach: call each cached function with representative arguments at startup.
from inhouse import MemoryStore, inhouse_cache
from inhouse.fastapi import create_lifespan
store = MemoryStore(max_size=2048, default_ttl=300)
@inhouse_cache(store=store)
def get_team_settings(team_id: int) -> dict:
...
@inhouse_cache(store=store)
def get_user_profile(user_id: int) -> dict:
...
async def warmup():
# Warm the most-requested teams
for team_id in [1, 2, 3, 4, 5]:
get_team_settings(team_id)
# Warm known user profiles
for user_id in popular_user_ids():
get_user_profile(user_id)
Call warmup() inside your FastAPI lifespan or before the server starts accepting requests.
Warmup with a lifespan
Tie warmup directly to the application lifecycle so it happens before the first request.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from inhouse import MemoryStore
from inhouse.fastapi import fastapi_cache
store = MemoryStore(max_size=4096, default_ttl=300)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Warmup before accepting requests
await warmup_popular_routes()
yield
# Cleanup on shutdown
app = FastAPI(lifespan=lifespan)
@app.get("/api/teams/{team_id}")
@fastapi_cache(store=store)
async def get_team(team_id: int) -> dict:
...
The warmup_popular_routes function runs before the first request is served. Cold-start latency goes to zero for warmed entries.
Warming multiple stores
If you have separate stores for different data tiers, warm each one independently.
hot_store = MemoryStore(max_size=512, default_ttl=30)
cold_store = MemoryStore(max_size=4096, default_ttl=3600)
@inhouse_cache(store=hot_store)
def get_recent_activity(team_id: int) -> list[dict]:
...
@inhouse_cache(store=cold_store)
def get_team_settings(team_id: int) -> dict:
...
async def warmup_all():
# Warm the hot store with the most active teams
active_teams = await db.fetch_all("SELECT id FROM teams ORDER BY activity DESC LIMIT 50")
for row in active_teams:
get_recent_activity(row["id"])
# Warm the cold store with all team settings
all_teams = await db.fetch_all("SELECT id FROM teams")
for row in all_teams:
get_team_settings(row["id"])
The hot store gets a smaller, targeted warmup. The cold store gets fully populated since it has a longer TTL and larger capacity.
Selective warmup based on traffic patterns
Use request logs to identify the most common cache keys and warm only those.
from collections import Counter
# Load top-N most requested keys from yesterday's traffic
TOP_KEYS = [
("get_team_settings", {"team_id": 5}),
("get_team_settings", {"team_id": 12}),
("get_user_profile", {"user_id": 101}),
("get_user_profile", {"user_id": 203}),
]
async def warmup_from_traffic():
for func_name, kwargs in TOP_KEYS:
if func_name == "get_team_settings":
get_team_settings(**kwargs)
elif func_name == "get_user_profile":
get_user_profile(**kwargs)
Your most popular entries are hot before any user requests them. Unpopular entries warm naturally on first access.
Warmup with pagination
For endpoints that return paginated results, warm the first few pages.
@inhouse_cache(store=store)
def list_products(category: str, page: int = 1, sort: str = "name") -> list[dict]:
...
async def warmup_catalog():
categories = ["electronics", "home-garden", "books", "clothing"]
for cat in categories:
for page in [1, 2, 3]:
list_products(category=cat, page=page)
The first three pages of every category are cached before anyone asks. Deeper pages load on demand.
Monitoring warmup success
Use MemoryStore.stats() to verify warmup worked.
async def warmup_and_verify():
before = store.stats()
print(f"Cache size before warmup: {before['size']}")
await warmup_all()
after = store.stats()
print(f"Cache size after warmup: {after['size']}")
print(f"Entries added: {after['sets'] - before['sets']}")
if after['size'] < EXPECTED_ENTRIES:
print("Warning: warmup may be incomplete")
If the cache size after warmup is lower than expected, some warmup calls may have failed silently or produced duplicate keys.
Graceful handling of partial failures
A warmup call that hits a database error should not crash the server.
async def safe_warmup(func, *args, **kwargs):
try:
func(*args, **kwargs)
except Exception as e:
print(f"Warmup failed for {func.__name__}: {e}")
# Continue warming other entries
async def warmup_with_fallback():
for team_id in range(1, 100):
await safe_warmup(get_team_settings, team_id=team_id)
The warmup skips failed entries and continues. The first real request for a missed entry will cache it normally.