Back to Blog
Python

Python DiskCache: Persistent Caching, TTL, Memoization

python diskcache persistent caching ttl and memoization: Use Python's diskcache library for persistent caching with TTL and memoization. Covers expiry, eviction, concu...

diskcachepersistent-cachememoizationttlpython-caching
Illustration of a Python function being memoized and its result stored in a persistent disk cache with a TTL timer showing expiry.

python diskcache persistent caching ttl and memoization requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

functools.lru_cache memoizes function results in process memory, which works well for a long-running service with a stable heap. But the cache is lost on restart, and it competes with application data for RAM. For a CLI tool that runs once, a worker that starts and stops frequently, or a service that redeploys often, the cache is effectively rebuilt on every run.

diskcache moves the cache to disk. It keeps a dictionary-like interface and a memoize decorator, while adding TTL support, larger capacity, and persistence across restarts. Using python diskcache for persistent caching with TTL and memoization is a practical way to keep expensive results across process lifetimes.

Creating a Persistent Cache

The entry point is diskcache.Cache, which manages a directory on disk:

from diskcache import Cache cache = Cache("/var/cache/myapp")

The first argument is the cache directory. If it does not exist, diskcache creates it. The Cache object behaves like a dictionary for the most common operations:

cache["user:42"] = {"name": "Ada", "role": "admin"} print(cache["user:42"])

Values are serialized with pickle by default, so any picklable Python object can be stored. The cache directory contains a SQLite database plus the serialized values; diskcache handles the bookkeeping internally.

One detail worth knowing: Cache is a context manager, and closing it flushes pending writes. In a short-lived script, using with ensures the cache is properly closed:

with Cache("/var/cache/myapp") as cache: cache["key"] = value

For long-running services, leaving the cache open is fine; diskcache writes through to disk as entries are set.

Setting a TTL on Cache Entries

TTL (time-to-live) controls how long an entry remains valid. diskcache implements this with the expire parameter on set:

cache.set("session:42", user_data, expire=3600)

After 3600 seconds, the entry is considered expired. diskcache does not immediately delete expired entries from disk; it removes them lazily when they are accessed or during cache maintenance. That means a get on an expired key returns a miss:

value = cache.get("session:42") # None after expiry

The get method accepts a default argument, which is useful when the entry may be missing or expired:

value = cache.get("session:42", default=None)

TTL is evaluated at read time, so an expired entry is indistinguishable from a missing one from the caller's perspective. This lazy expiry keeps writes cheap; there is no background sweeper running on every set.

Memoizing Functions with the Decorator

The memoize decorator caches a function's return value keyed by its arguments:

from diskcache import Cache cache = Cache("/var/cache/myapp") @cache.memoize() def fetch_user(user_id): # expensive work: database query, HTTP call, computation return {"id": user_id, "name": "Ada"}

The first call with a given user_id executes the function and stores the result. Subsequent calls with the same argument return the cached value without running the body. The decorator builds the cache key from the function name and the serialized arguments, so different argument values produce different keys.

This is the same idea as functools.lru_cache, but the cache survives process restarts and can grow far beyond available RAM.

Combining TTL with Memoization

The memoize decorator accepts an expire argument, which applies a TTL to every entry it creates:

@cache.memoize(expire=300) def fetch_user(user_id): return query_user_from_db(user_id)

Now each cached result is valid for five minutes. After that, the next call executes the function again and refreshes the entry. This is the pattern that matters for python diskcache persistent caching ttl and memoization: the function result is persisted to disk, expires after a fixed window, and is recomputed on demand.

The expire value is a number of seconds. There is no separate ttl parameter on memoize; expire is the TTL. Setting it to None (the default) keeps entries indefinitely.

A common production pattern is to read the TTL from configuration so it can be tuned without redeploying code:

import os ttl = int(os.environ.get("USER_CACHE_TTL", "300")) @cache.memoize(expire=ttl) def fetch_user(user_id): return query_user_from_db(user_id)

What Happens When the Cache Is Full

diskcache has a size_limit that defaults to 1 GB. When the cache exceeds that limit, it evicts entries using a least-recently-used (LRU) policy. Expired entries are the first to go, followed by the least recently accessed live entries.

The limit can be configured at construction time:

cache = Cache("/var/cache/myapp", size_limit=5_000_000_000)

Eviction is important to understand because it interacts with TTL. A TTL guarantees that an entry will not be returned after it expires, but it does not guarantee that a live entry will still be present. If the cache is under memory pressure, live entries can be evicted early. Code that uses the cache must therefore treat every read as potentially missing, regardless of TTL.

Concurrency and Thread Safety

diskcache is designed for concurrent access. Multiple processes can open the same cache directory, and SQLite coordinates access between them. Reads are cheap; writes acquire a lock briefly. This makes diskcache a reasonable choice for multi-process web workers that share a cache directory, which is a common deployment shape for Python services behind gunicorn or uvicorn.

The memoize decorator is safe under concurrency in the sense that concurrent writes to the same key do not corrupt the cache. When two processes call the same memoized function with the same arguments at the same time, both may compute the result, but the cache remains consistent; one write simply replaces the other. If the function is expensive and many workers can call it simultaneously, consider accepting the occasional duplicate computation or adding an application-level lock.

There is one caveat: the cache directory must be on a filesystem that supports SQLite locking semantics. Network filesystems such as NFS can behave unpredictably with SQLite's locking, so keep the cache directory on local storage.

Operational Considerations

The main tradeoff of persistent caching is I/O. Every read and write touches disk, so diskcache is slower than an in-memory cache for individual operations. The practical benefit is that the cache survives restarts, which often matters more than per-operation latency for expensive computations.

A useful pattern is a two-tier cache: a small in-memory layer for the hottest entries, backed by diskcache for everything else. The in-memory layer can be a plain dict or functools.lru_cache, with diskcache as the fallback. This keeps the hottest reads fast while retaining persistence for the long tail.

Cache invalidation is the other operational concern. TTL handles time-based staleness, but not explicit invalidation. If the underlying data changes before the TTL expires, the cache returns stale data. diskcache provides delete and clear for manual invalidation:

cache.delete("user:42") cache.clear() # remove every entry

For memoized functions, the key is derived from the function and arguments, so deleting a specific entry requires knowing the key. A simpler approach is to use a version number in the function name or to clear the cache when a schema changes. In practice, choose a TTL short enough that stale data is acceptable for the application's requirements.

python diskcache persistent caching ttl and memoization: Pra | RYUSLOG DEV