skip to content

With Django's low-level cache API, how do cache.set(), cache.get() and cache.add() behave, and what do timeout=None and timeout=0 mean?

level: juniorimportance: must knowfreq 62%

answer

  1. any picklable value
  2. a miss returns the default
  3. add only when absent
  4. None forever, 0 not at all

basics

~20 s

Django's cache.set(key, value, timeout) stores a picklable value, cache.get(key, default) returns it or the default on a miss, and cache.add() stores only if the key is absent. timeout=None means never expire; timeout=0 means do not cache.

solid answer

~40 s

`from django.core.cache import cache` gives the default alias. `cache.set("leaderboard:weekly", rows, 300)` stores any picklable object for 300 seconds; leaving `timeout` out uses the alias's `TIMEOUT` (default 300). `cache.get(key)` returns the value or `None` on a miss, or the `default` you pass; to tell a cached `None` from a miss, pass a sentinel such as `object()`. `cache.add(key, value)` writes only when the key is not already present and returns `True` or `False`. For the timeout, `None` stores the value with no expiry and `0` does not cache it, so `set(key, value, 0)` effectively removes whatever was there. Every method also has an `a`-prefixed twin for async code, such as `await cache.aget(key)`.

go deeper

for a junior

Recall set, get with a default, add, and that None means forever while 0 means not cached.

for a middle

Explain the TIMEOUT fallback, pickling, the sentinel pattern for cached None, and add's return value.

for a senior

Choose timeouts deliberately per value, and use add's first-writer-wins behaviour where concurrent writers meet.

for a principal

Set conventions for keys, timeouts and what may be cached so a team's cache usage stays predictable.

## The low-level API in one import Django's **low-level cache API** is for caching values, not whole pages: a computed leaderboard, the result of an expensive query, a rendered snippet, an API response. You import a backend object and call methods on it: ```python from django.core.cache import cache # the "default" alias from django.core.cache import caches # caches["other"] for other aliases ``` Every built-in backend implements the same methods, so code does not change when the backend does. ## set, get and add | Method | What it does | Returns | |---|---|---| | `cache.set(key, value, timeout=..., version=None)` | Stores or overwrites the value | `None` | | `cache.get(key, default=None, version=None)` | Reads the value | The value, or `default` on a miss | | `cache.add(key, value, timeout=..., version=None)` | Stores only if the key is absent | `True` if stored, `False` otherwise | - **Keys** should be strings. Keep them short and free of spaces: the Memcached backends reject keys over 250 characters or containing whitespace, and the other backends warn about them. - **Values** can be any **picklable** Python object: lists of dicts, model instances, dataclasses. Lambdas, open files and database connections cannot be pickled. - **`add()`** is the "only if empty" write. It is the building block for "first writer wins" logic, and on Redis and Memcached it maps to an atomic server-side operation. ## Timeout semantics The `timeout` argument is in **seconds**: 1. **Omitted**: uses the alias's `TIMEOUT` from `CACHES`, which defaults to `300`. 2. **A positive number**: expire after that many seconds. 3. **`None`**: store with **no expiry**. The entry stays until it is deleted, overwritten or evicted by the backend. 4. **`0`**: **do not cache**. The value expires immediately; on several backends a `set()` with `0` simply deletes the existing key. Confusing `0` with "forever" is the classic mistake: code meant to cache a leaderboard permanently with `timeout=0` caches nothing, and every request recomputes it. ## Telling a cached None from a miss `cache.get()` returns `None` both when the key is missing and when the stored value is `None`. If `None` is a legitimate result, for example "no leader yet this week", pass a sentinel: ```python MISSING = object() value = cache.get("leaderboard:weekly", MISSING) if value is MISSING: value = compute_leaderboard() cache.set("leaderboard:weekly", value, 300) ``` ## Related calls worth knowing - **`cache.has_key(key)`** and **`key in cache`** report whether a live, unexpired entry exists. - **`cache.get_or_set(key, default, timeout)`** combines the read, the fallback and the write; `default` may be a callable. - **`cache.delete(key)`** removes one entry and returns whether a key was deleted. - **Async twins**: `aget`, `aset`, `aadd`, `adelete` and the rest take the same arguments and are awaited in async views. ## Choosing keys and values A few habits keep low-level caching predictable: - **Namespace keys by purpose**: `leaderboard:weekly`, `leaderboard:weekly:page:2`, `player:42:rank`. Readable keys are easy to delete deliberately and easy to find when debugging. - **Put every input in the key**: if the leaderboard depends on the season and the region, both belong in the key, otherwise one region's rows are served to another. - **Cache plain data rather than live objects**: a list of dicts or tuples pickles small and survives code changes better than full model instances, which drag their whole field state and class path into the pickle. - **Keep values modest**: large pickles cost time to serialise on every write and read, and Memcached rejects values above its item size limit. - **Pick the timeout from how stale the data may be**, not from how expensive it is to compute; expensive data that must be fresh needs invalidation, not a long timeout. ## A leaderboard example A view shows the weekly leaderboard, which needs an expensive aggregate query: - Read with `cache.get("leaderboard:weekly")`. - On a miss, run the query, then `cache.set("leaderboard:weekly", rows, 300)`. - When a match result is recorded, call `cache.delete("leaderboard:weekly")` so the next reader rebuilds it. That is the whole pattern at junior level; the traps come from concurrency, invalidation and the choice of backend.

  • Why can cache.get() not tell you whether a key holds None?
    `cache.get()` returns its `default`, which is `None` unless you pass another value, both on a miss and when the stored value is `None`. Passing a unique sentinel such as `object()` as the default separates the two cases, because a miss returns that exact object.
  • What does cache.add() return, and when would you use it instead of set()?
    It returns `True` if it stored the value and `False` if the key already existed. Use it when the first writer must win, for example initialising a counter before `incr()`, or claiming a short-lived marker so only one worker rebuilds an expensive value.

A timeout of None is a note pinned to the board with no date; a timeout of 0 is a note that you throw away the moment you write it.

saying these in an interview costs you the question

  • timeout=0 caches the value forever.
  • Omitting timeout stores the value with no expiry.
  • cache.get() raises KeyError when the key is missing.
  • cache.add() overwrites an existing value like set().
  • Only strings and numbers can be stored in Django's cache.