Skip to main content

Caching Framework and Backends

The caching framework in this codebase provides a unified interface for interacting with various storage backends. By abstracting the underlying storage—whether it is a database, memory, or a dedicated service like Redis—the framework allows developers to implement caching strategies that are portable and consistent across the application.

Core Caching Interface​

The foundation of the caching system is the BaseCache class located in django/core/cache/backends/base.py. This abstract base class defines the standard API that all backends must implement.

Standard Operations​

Every cache backend provides a set of core methods for data manipulation:

  • get(key, default=None, version=None): Retrieves a value.
  • set(key, value, timeout=DEFAULT_TIMEOUT, version=None): Stores a value.
  • add(key, value, timeout=DEFAULT_TIMEOUT, version=None): Stores a value only if the key does not already exist.
  • delete(key, version=None): Removes a key.
  • touch(key, timeout=DEFAULT_TIMEOUT, version=None): Updates the expiration time of an existing key.

Key Construction and Validation​

The BaseCache handles key normalization through make_key. By default, it combines a KEY_PREFIX, the version, and the user-provided key.

# From django/core/cache/backends/base.py
def make_key(self, key, version=None):
if version is None:
version = self.version
return self.key_func(key, self.key_prefix, version)

Backends also perform validation via validate_key. For example, Memcached backends (subclasses of BaseMemcachedCache) will raise an InvalidCacheKey exception if a key exceeds 250 characters or contains illegal control characters, ensuring compatibility with the Memcached protocol.

Database Backend​

The DatabaseCache (in django/core/cache/backends/db.py) stores cached data in a relational database table. This is useful for environments where a dedicated caching server like Redis is not available.

Storage and Serialization​

Data is stored using a combination of pickle and base64 encoding. When a value is set, it is pickled and then base64-encoded to ensure it can be stored in a standard database text or blob column.

# From django/core/cache/backends/db.py
pickled = pickle.dumps(value, self.pickle_protocol)
b64encoded = base64.b64encode(pickled).decode("latin1")

Culling Strategy​

Because databases do not automatically expire rows, DatabaseCache implements a manual culling mechanism. When the number of entries exceeds MAX_ENTRIES, the _cull method is triggered. It first deletes expired entries and then, if the cache is still too full, deletes a portion of the oldest entries based on the CULL_FREQUENCY setting.

Memcached Backends​

The BaseMemcachedCache class in django/core/cache/backends/memcached.py serves as the base for backends using libraries like pylibmc or pymemcache.

The 30-Day Timeout Rule​

A specific quirk of Memcached is how it handles long timeouts. Any timeout value greater than 2,592,000 seconds (30 days) is interpreted by Memcached as a Unix timestamp rather than a relative offset. BaseMemcachedCache handles this conversion automatically in get_backend_timeout:

# From django/core/cache/backends/memcached.py
if timeout > 2592000: # 30 days
# Switch to absolute timestamps for Memcached compatibility
timeout += int(time.time())
return int(timeout)

Redis Backend​

The RedisCache implementation in django/core/cache/backends/redis.py delegates operations to a RedisCacheClient. This backend is highly optimized for performance and supports advanced features like connection pooling.

Atomic Increments​

Unlike other backends that might fetch, increment, and re-set a value, RedisCache utilizes Redis's native INCR command for atomicity. To support this, the RedisSerializer specifically avoids pickling integer values:

# From django/core/cache/backends/redis.py
class RedisSerializer:
def dumps(self, obj):
# For better incr() and decr() atomicity, don't pickle integers.
if type(obj) is int:
return obj
return pickle.dumps(obj, self.protocol)

Connection Management​

The RedisCacheClient manages multiple connection pools. It can be configured to write to a primary server while reading from multiple replicas, distributing the load across a Redis cluster.

Asynchronous Support​

All cache backends in this framework support asynchronous operations. BaseCache provides a-prefixed versions of all standard methods (e.g., aget, aset, adelete).

If a specific backend does not provide a native async implementation, BaseCache falls back to running the synchronous method in a thread using asgiref.sync.sync_to_async:

# From django/core/cache/backends/base.py
async def aget(self, key, default=None, version=None):
return await sync_to_async(self.get, thread_sensitive=True)(
key, default, version
)

This ensures that the caching API remains consistent regardless of whether the application is running in a synchronous or asynchronous execution context.