Skip to main content

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.