skip to content

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%

answer

  1. remove, remove many, extend
  2. incr_version moves one key
  3. default-version readers then miss
  4. a generation number for groups

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.

solid answer

~40 s

`cache.delete(key)` removes one entry and returns whether a key was deleted; `cache.delete_many(keys)` removes several, in one round trip on Redis, Memcached and the database cache. `cache.touch(key, timeout)` sets a new expiry on an existing key without rewriting its value and returns `False` if the key is missing; `touch(key, None)` makes it permanent. `cache.incr_version(key)` reads the value at the current version, writes it at version + 1, deletes the old one and returns the new version; a later `cache.get(key)` with the default version then misses. So for leaderboards, delete the affected keys when results change. For a whole group, such as every page of every leaderboard, keep a generation number in the cache, pass it as `version=` on every get and set, and bump it with `incr()` to retire the whole group at once.

go deeper

for a junior

Recall that delete removes a key and touch changes how long it lives.

for a middle

Explain what incr_version actually moves, why default-version readers miss afterwards, and delete_many's batching.

for a senior

Invalidate groups of keys with a generation number passed as version, never with clear() on a shared cache.

for a principal

Define an invalidation contract per cached dataset so every writer knows which keys or generation to retire.

## Four calls, four jobs | Call | Effect | Returns | |---|---|---| | `cache.delete(key)` | Removes one entry | `True` if a key was deleted | | `cache.delete_many(keys)` | Removes each listed key | Nothing | | `cache.touch(key, timeout)` | Sets a new expiry on an existing key; value untouched | `True`, or `False` if the key is missing | | `cache.incr_version(key, delta=1)` | Moves the value to a higher version number | The new version | Each has an `a`-prefixed async twin: `adelete`, `adelete_many`, `atouch`, `aincr_version`. ## delete and delete_many `delete()` is the everyday invalidation call: when a match result is recorded, `cache.delete("leaderboard:weekly")` makes the next reader rebuild. `delete_many()` takes an iterable of keys. The base implementation, used by `FileBasedCache` and `LocMemCache`, loops over `delete()`, while the Redis and Memcached backends send one multi-key command and `DatabaseCache` runs one `DELETE ... IN` query, which matters when a result invalidates dozens of leaderboard pages at once. Deleting a missing key is not an error. ## touch `touch()` changes **only the expiry**: - `cache.touch("leaderboard:weekly", 600)` gives the entry another ten minutes. - `cache.touch(key, None)` removes the expiry entirely, on backends that support it. - It returns `False` for a missing or expired key, so it cannot resurrect anything. It is useful for "keep this alive while people are looking at it" without paying to serialise the value again. ## Versions and incr_version Every key Django stores includes a **version**. It comes from the alias's `VERSION` setting (default `1`) unless a call passes `version=`: ```python cache.set("leaderboard:weekly", rows, version=2) cache.get("leaderboard:weekly") # None: default version 1 cache.get("leaderboard:weekly", version=2) # rows ``` `cache.incr_version(key)` does three things: reads the value at the current version (default or given), writes it at `version + delta`, and deletes the old entry. It raises `ValueError` if the key is missing. The consequence surprises people: after `cache.incr_version("leaderboard:weekly")`, plain `cache.get("leaderboard:weekly")` returns `None`, because readers still ask for the default version while the value now lives at version 2. Nothing about the data changed; it moved. `decr_version()` moves it back down. `incr_version()` is therefore a tool for **migrating one key between versions**, not a way to invalidate a group of keys. ## Invalidating a group: a generation number To retire every leaderboard key at once, whatever their names, store a **generation** in the cache and use it as the version on every read and write: 1. `cache.add("leaderboard:generation", 1, None)` initialises it. 2. Readers and writers call `gen = cache.get("leaderboard:generation", 1)` and pass `version=gen` to `get`, `set` and `get_or_set`. 3. When results change, `cache.incr("leaderboard:generation")` bumps it. 4. Old entries are never read again and expire on their own timeouts. This avoids enumerating keys and never calls `clear()`, which would wipe the whole cache. ## A worked leaderboard example A site caches leaderboards per region and page: `leaderboard:eu:page:1`, `leaderboard:eu:page:2`, `leaderboard:us:page:1` and so on. - **A single match in the EU finishes**: `cache.delete_many([f"leaderboard:eu:page:{n}" for n in range(1, 11)])` retires the EU pages in one round trip on Redis. - **Traffic is high and the data is unchanged**: `cache.touch("leaderboard:eu:page:1", 600)` extends the hot first page without rewriting it. - **The scoring rules change for every region**: bump `leaderboard:generation` with `cache.incr()`; every reader switches to the new version on its next request, and no key names need to be listed. - **Someone reaches for `cache.incr_version("leaderboard:eu:page:1")`**: the page moves to a new version and readers using the generation or default version miss it, which is a confusing way to delete one key. ## Choosing - **One known key changed**: `delete()`. - **A known list changed**: `delete_many()`. - **Value still valid, just keep it longer**: `touch()`. - **Many keys, names unknown**: a generation number passed as `version=`. - **A release changes the shape of all cached data**: bump the alias's `VERSION` in `CACHES`, which is configuration rather than a call.

  • After cache.incr_version('leaderboard:weekly'), what does cache.get('leaderboard:weekly') return?
    `None`, assuming the alias's `VERSION` is 1. `incr_version()` moved the value to version 2 and deleted the version-1 entry, while a plain `get()` still asks for the default version 1. You must pass `version=2` to read it.
  • Why is delete_many() preferable to a loop of delete() calls on Redis?
    `RedisCache.delete_many()` builds all the keys and sends one multi-key delete, so invalidating fifty leaderboard pages costs one round trip instead of fifty. `DatabaseCache` likewise issues a single `DELETE ... IN` query, while the base implementation used by the file-based and local-memory backends simply loops over `delete()`.

saying these in an interview costs you the question

  • incr_version() invalidates every key sharing the same prefix.
  • After incr_version(), a plain cache.get() returns the value as before.
  • touch() re-creates a key that has already expired.
  • delete() raises an error when the key does not exist.
  • The only way to invalidate a group of keys is cache.clear().