How to Memoize Async Functions with lru_cache in Python
Cache async function results with functools.lru_cache to avoid repeated expensive awaits, cutting total execution from ~0.4s to ~0.2s in this example.
Python code
26 linesfrom functools import lru_cache
import asyncio
@lru_cache(maxsize=128)
async def fetch_data(user_id: int) -> str:
# Simulate expensive async operation
await asyncio.sleep(0.1)
return f"Data for user {user_id}"
async def main():
start = asyncio.get_event_loop().time()
# First calls (miss cache)
print(await fetch_data(1))
print(await fetch_data(2))
# Second calls (hit cache - much faster)
print(await fetch_data(1))
print(await fetch_data(2))
elapsed = asyncio.get_event_loop().time() - start
print(f"Total time: {elapsed:.2f}s")
print(f"Cache info: {fetch_data.cache_info()}")
if __name__ == "__main__":
asyncio.run(main())
Output
Data for user 1
Data for user 2
Data for user 1
Data for user 2
Total time: 0.20s
Cache info: CacheInfo(hits=2, misses=2, maxsize=128, currsize=2)
How it works
lru_cache wraps fetch_data so cached values are stored in a dict keyed by the arguments (user_id). On the first call for user 1, the coroutine runs and await asyncio.sleep(0.1) executes; on the second call for user 1, the cached str result is returned immediately without re-suspending. Because the cache stores the awaited result rather than a coroutine, repeated calls skip the sleep. The cache_info() method exposes hits, misses, and size, which is handy for tuning maxsize. Note that the cache is shared across all event loops, so keys must be hashable and the function stays process-wide.
Common mistakes
- Forgetting to call `await` – lru_cache caches the coroutine object, not the awaited value
- Using lru_cache on functions that take unhashable arguments like lists or dicts (must be tuples/frozensets)
- Expecting cache to clear automatically across task runs – use `cache_clear()` from the loop
- Sharing mutable default args with cached functions causes stale data on repeated calls
Variations
- Use `cachetools.TTLCache` or `async_cache` from `async_lru` for time-based async caching
- Wrap with a custom decorator that awaits and caches on success only
Real-world use cases
- Caching API responses or database lookups in an async web server to cut redundant network round-trips.
- Memoizing expensive computations inside an asyncio worker so repeated job inputs return instantly.
- Storing fetched user profiles or config values in a bot/daemon and refreshing only on explicit invalidation.
Sponsored
More from Concurrency & performance
- Benchmark list.append vs deque.append in Python medium
- Build a Python Performance Profiler That Generates Readable Reports medium
- Graceful Shutdown Executor Context Manager in Python medium
- How to Build a Producer-Consumer Pattern with asyncio.Queue in Python medium
- How to Cancel an asyncio Task with Graceful Cleanup in Python medium
- How to Convert Data in Parallel with ThreadPoolExecutor in Python easy
Keep learning
Related tutorials and quizzes for this topic.