skip to content

Low-Level Calls & Fragments

The low-level API stores any picklable value with get, set, add and get_or_set, and the {% cache %} tag stores template fragments. Interviewers probe timeout semantics and invalidation.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

6

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.
open as a page

How does Django's {% cache %} template tag cache a fragment per user or language, and how do you invalidate that fragment from Python code?

level: middleimportance: must knowfreq 48%

basics

~20 s

Django's {% cache timeout name var1 var2 %} stores the rendered block under a key from the fragment name and the extra arguments' string values. To invalidate, rebuild it with make_template_fragment_key(name, [vars]) and delete it from the same alias.

open as a page

In Django's cache API, how do delete(), delete_many(), touch() and incr_version() differ when you need to invalidate or extend cached leaderboard entries?

level: middleimportance: should knowfreq 32%

basics

~20 s

Django's delete() removes one key, delete_many() removes a list, and touch() only changes an existing key's expiry. incr_version() moves one key's value to a higher version, so readers using the default version miss it; it is not a group invalidation.

open as a page

Is Django's cache.incr() atomic, and what happens when you call it on a key that does not exist yet?

level: middleimportance: should knowfreq 34%

basics

~10 s

Django's cache.incr() is atomic only on backends with a native increment, like Memcached and Redis; others read then write. A missing key raises ValueError, so initialise the counter with cache.add(key, 0) first.

open as a page

A Django leaderboard is cached with cache.get_or_set(key, compute_leaderboard, 300); when the entry expires under heavy traffic the database spikes. What does get_or_set actually do, and how would you fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Django's get_or_set is get, call the default, add, get again, with no lock, so every request missing an expired hot key runs the expensive callable. Refresh the value ahead of expiry, or let one worker claim the rebuild with add().

open as a page

In an async Django view, what do cache.aget() and cache.aset() actually do with the built-in cache backends?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Django's aget() and aset() are async wrappers: with every built-in backend they run the synchronous method through sync_to_async in a worker thread. They keep the event loop unblocked, but the cache I/O itself is not asynchronous.

open as a page