Skip to main content

Surgical Cache Invalidation

You have an API that caches user profiles. When a user updates their profile, you need to evict exactly their entry - not every entry, not even every user entry. Just that one.

Before v1.0, you had two options: cache_clear() (nukes everything for that function) or manual store.delete() (requires knowing the exact key format). Neither is great.

cache_invalidate() builds the same key your function would use and deletes it. One call, one entry gone.

The problem: a user profile API

from inhouse import inhouse_cache, MemoryStore

store = MemoryStore(max_size=1024, default_ttl=300)

@inhouse_cache(store=store)
def load_profile(db: object, user_id: int) -> dict:
row = db.execute(
"SELECT name, email, avatar FROM users WHERE id = ?", (user_id,)
).fetchone()
return dict(row) if row else None

The db argument is a database connection. Every connection object is different, so the same user_id with a different db handle produces a different cache key. That means cache misses even for identical queries.

Fix: exclude the database connection

With exclude=, you drop db from the key. Now load_profile(conn_a, 42) and load_profile(conn_b, 42) share one entry.

@inhouse_cache(store=store, exclude=("db",))
def load_profile(db: object, user_id: int) -> dict:
row = db.execute(
"SELECT name, email, avatar FROM users WHERE id = ?", (user_id,)
).fetchone()
return dict(row) if row else None

No custom key_builder. No manual key construction. One parameter name and it is done.

Evict one entry after a write

The user updates their avatar. The profile page should show the new image on the next load.

def update_avatar(db: object, user_id: int, new_avatar: str):
db.execute(
"UPDATE users SET avatar = ? WHERE id = ?", (new_avatar, user_id)
)
db.commit()

# evict exactly this user's cached profile
load_profile.cache_invalidate(db, user_id=user_id)

The key is built with the same exclude=("db",) rule, so the db argument is ignored. The user_id=42 part determines which key to delete. The next call to load_profile(any_db, 42) is a cache miss and refreshes from the database.

Multiple parameters, surgical eviction

A dashboard with filters needs to evict one combination of parameters when data changes.

@inhouse_cache(store=store, exclude=("db",))
def load_usage_report(db: object, team_id: int, period: str, region: str | None = None) -> dict:
...

# New data ingested for team 5, Q3, US region:
load_usage_report.cache_invalidate(db, team_id=5, period="Q3", region="US")

Only the entry for team_id=5, period="Q3", region="US" is evicted. Other teams, other periods, other regions are untouched.

Check if the eviction actually happened

cache_invalidate() returns True if a key was removed, False if it did not exist.

if load_profile.cache_invalidate(db, user_id=999):
print("Evicted user 999's cached profile")
else:
print("User 999 was not in cache (maybe TTL expired)")

Useful for logging or conditional logic in write paths.

Multiple stores, same pattern

If you have hot and cold stores, cache_invalidate works on whichever store the decorator was configured with.

hot_store = MemoryStore(default_ttl=30)
cold_store = MemoryStore(default_ttl=3600)

@inhouse_cache(store=hot_store, exclude=("db",))
def load_recent_activity(db: object, team_id: int) -> list[dict]:
...

@inhouse_cache(store=cold_store, exclude=("db",))
def load_team_settings(db: object, team_id: int) -> dict:
...

# After a new event is ingested:
load_recent_activity.cache_invalidate(db, team_id=5)
# Team settings stay cached - different function, different store

When to use each invalidation method

MethodScopeUse case
cache_invalidate(*args, **kwargs)One call signatureAfter a write that affects a specific entity
cache_clear()All entries for a functionAfter a bulk operation or schema change
store.delete(prefix)Custom key prefixWhen you know the exact key format
store.clear()Entire storeEmergency flush

The recipe in production

from inhouse import MemoryStore, inhouse_cache

store = MemoryStore(max_size=2048, default_ttl=60)

@inhouse_cache(store=store, exclude=("db",))
def get_user_profile(db, user_id: int) -> dict | None:
...

@inhouse_cache(store=store, exclude=("db",))
def get_user_orders(db, user_id: int, limit: int = 10) -> list[dict]:
...

def place_order(db, user_id: int, order_data: dict) -> int:
order_id = insert_order(db, order_data)
db.commit()

# Evict this user's cached data
get_user_profile.cache_invalidate(db, user_id=user_id)
get_user_orders.cache_invalidate(db, user_id=user_id)
return order_id

The profile and orders cache entries for user_id=42 are evicted after the write. Every other user's cache is untouched.