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.