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.

Easy Python 3.9+ Aug 9, 2026 Concurrency & performance 13 views 0 copies

Python code

26 lines
Python 3.9+
from 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

stdout
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

  1. Use `cachetools.TTLCache` or `async_cache` from `async_lru` for time-based async caching
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Concurrency & performance

Related tutorials and quizzes for this topic.