Cache Invalidation
async-cache provides invalidation at every API level: core AsyncCache,
decorators (AsyncLRU / AsyncTTL), and agent cache (AgentCacheInvalidator).
Core AsyncCache
from cache import AsyncCache
cache = AsyncCache(maxsize=1000, default_ttl=300)
# Delete a single key
cache.delete("user:123")
# Clear all entries and reset metrics
cache.clear()
Decorator API: Per-Key Invalidation
Every @AsyncLRU and @AsyncTTL wrapper exposes invalidate_cache():
from cache import AsyncLRU
@AsyncLRU(maxsize=128)
async def get_user(user_id: int):
return await db.get_user(user_id)
# Invalidate a specific cached result
get_user.invalidate_cache(42) # removes cache entry for get_user(42)
# Clear the entire cache
get_user.clear_cache()
Invalidation Decorators
For write/mutation functions that should automatically invalidate related cached
reads, use AsyncLRUInvalidator or AsyncTTLInvalidator.
Important: Invalidation happens before the mutation executes. This ensures no reader ever sees stale data after a mutation starts, even if the mutation raises an exception.
AsyncLRUInvalidator
from cache import AsyncLRU, AsyncLRUInvalidator
@AsyncLRU(maxsize=128)
async def get_user(user_id: int):
return await db.get_user(user_id)
@AsyncLRUInvalidator(get_user)
async def update_user(user_id: int, data: dict):
await db.update_user(user_id, data)
await get_user(1) # miss -> loads from DB
await get_user(1) # hit
await update_user(1, {"name": "Alice"}) # invalidates get_user(1), then runs mutation
await get_user(1) # miss -> reloads fresh data
AsyncTTLInvalidator
from cache import AsyncTTL, AsyncTTLInvalidator
@AsyncTTL(time_to_live=60)
async def get_session(session_id: str):
return await db.get_session(session_id)
@AsyncTTLInvalidator(get_session)
async def end_session(session_id: str):
await db.delete_session(session_id)
await get_session("s1") # miss -> loads
await end_session("s1") # invalidates get_session("s1"), then runs mutation
await get_session("s1") # miss -> reloads
Invalidator Options
Parameter |
Default |
Description |
|---|---|---|
|
(required) |
The |
|
False |
Clear the entire cache instead of a specific key |
|
None |
Custom |
Note
skip_args has been removed from invalidators. Passing a non-zero
value raises TypeError. The old skip_args on invalidators silently
produced wrong cache keys when the cached function also used skip_args.
Use key_fn instead — see the example below.
Mutation Failure Safety
The cache key is evicted before the mutation runs. If the mutation raises, the key is already gone — the next read triggers a fresh load:
@AsyncLRUInvalidator(get_user)
async def update_user(user_id: int, data: dict):
raise RuntimeError("DB error") # mutation fails
await get_user(1) # loads and caches
try:
await update_user(1, {"x": "y"}) # invalidates first, then raises
except RuntimeError:
pass
await get_user(1) # cache miss -> reloads fresh data
Clear All Mode
When a mutation affects all cached entries:
@AsyncLRUInvalidator(get_user, clear_all=True)
async def rebuild_user_index():
await db.rebuild_index()
await rebuild_user_index() # clears ALL get_user cache entries
Custom Key Mapping (key_fn)
When the mutation function’s args don’t directly match the cached function’s
args, use key_fn to translate them:
@AsyncLRU(maxsize=128)
async def get_item(item_id: int):
return await db.get_item(item_id)
# Mutation has extra args; extract just item_id for invalidation
@AsyncLRUInvalidator(get_item, key_fn=lambda args, kw: (args[:1], {}))
async def update_item(item_id: int, name: str, price: float):
await db.update_item(item_id, name=name, price=price)
Migrating from ``skip_args``: if you previously used skip_args=1 on the
invalidator to skip self, replace it with key_fn:
# Old (raises TypeError now):
# @AsyncLRUInvalidator(get_data, skip_args=1)
# New:
@AsyncLRUInvalidator(get_data, key_fn=lambda args, kw: (args[1:], {}))
async def update_data(self, key):
...
AgentCache Invalidation
For AI agent workflows, use resource-based invalidation. Like the decorator
invalidators, AgentCacheInvalidator evicts the cache before the mutation
runs — failed mutations never leave stale data.
from agent_cache import AgentCache, AgentCacheInvalidator
@AgentCache(resource="cart", scope="global", ttl=60)
async def get_cart(user_id):
return await db.get_cart(user_id)
@AgentCacheInvalidator(resource="cart", scope="global")
async def add_to_cart(user_id, item):
await db.add_item(user_id, item)
await get_cart("u1") # cached
await add_to_cart("u1", "laptop") # invalidates ALL "cart" entries, then runs mutation
await get_cart("u1") # re-fetched
AgentCacheInvalidator accepts the same invalidation parameters as the
decorator invalidators:
Parameter |
Default |
Description |
|---|---|---|
|
(required) |
Resource tag to invalidate |
|
|
|
|
True |
Clear all entries for the resource (default for agent cache) |
|
None |
Custom |