Is Django's cache.incr() atomic, and what happens when you call it on a key that does not exist yet?
answer
- depends on the backend
- native increment versus read-then-write
- missing key raises
- initialise with add first
basics
~10 sDjango'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.
solid answer
~40 s`cache.incr(key, delta=1)` and `cache.decr()` add to a stored number and return the new value. The docs say they are **not guaranteed to be atomic**: the Memcached backends call the server's increment, `RedisCache` stores integers unpickled and uses Redis's own increment command, and `LocMemCache` increments under its lock within one process, but `DatabaseCache` and `FileBasedCache` inherit the base implementation, which reads the value and writes it back, so concurrent increments can be lost; that write-back also resets the entry to the alias's default timeout. Every built-in backend raises `ValueError` for a missing key, so the safe idiom for a leaderboard view counter is `cache.add(key, 0, timeout)` followed by `cache.incr(key)`. For counts that must not be lost, persist them in the database.
go deeper
Recall that incr and decr change a stored number and that the key must exist first.
Explain which backends increment natively, the ValueError on missing keys, and the add-then-incr idiom.
Spot lost updates and timeout resets from the base fallback, and move exact counts to the database.
Classify counters as approximate or exact and choose the store for each accordingly.
## What incr and decr do `cache.incr(key, delta=1, version=None)` adds `delta` to the stored value and returns the result; `cache.decr()` subtracts. Their async twins are `aincr()` and `adecr()`. They are handy for counters: page views on a leaderboard, rate-limit hits, a generation number. ## Atomicity depends on the backend Django's documentation is explicit: `incr()` and `decr()` are **not guaranteed to be atomic**. Backends with a native increment are atomic; the rest fall back to a two-step read and write. | Backend | Implementation | Atomic? | |---|---|---| | `PyMemcacheCache`, `PyLibMCCache` | Server `incr` / `decr` | Yes, across all clients | | `RedisCache` | Checks the key exists, then Redis's increment command on an unpickled integer | The increment itself, yes | | `LocMemCache` | Read and write under the store's lock | Within one process only | | `DatabaseCache`, `FileBasedCache` | Base `get()` then `set()` | No | With the base implementation, two requests can both read `41`, both write `42`, and one increment is lost. On a busy counter, losses are routine rather than rare. The Redis backend deliberately leaves plain integers unpickled when it stores them, precisely so that `INCR` on the server works and stays atomic. ## Missing keys raise ValueError Every built-in backend raises **`ValueError`** ("Key '...' not found") when you increment a key that does not exist or has expired. Django never creates the key for you. The idiom is: ```python from django.core.cache import cache key = "leaderboard:views:2026-09-26" cache.add(key, 0, timeout=86400) # no-op if the counter already exists views = cache.incr(key) ``` `add()` writes only when the key is absent, so it is safe to call on every request. There is still a small window where the key can expire between `add()` and `incr()`; catch `ValueError` and retry if that matters. ## Timeouts and incr - The **base** implementation writes the new value with `set()` and no timeout argument, so the entry gets the alias's **default `TIMEOUT`** again (300 seconds unless configured). A counter created with a 24-hour timeout on the database cache quietly shrinks to five minutes after the first increment. - `LocMemCache` updates the value in place and keeps the original expiry; the Memcached and Redis backends increment on the server, which does not change the key's expiry. - If exact lifetime matters, call `cache.touch(key, timeout)` after incrementing, or keep the counter on a backend whose increment is native. ## Values that are not integers `incr()` expects the stored value to support `+`. On the base implementation any such object works; on Redis and Memcached the stored value must be an integer, because the server does the arithmetic. Store counters as plain `int`. ## A rate-limit example A soft limit of 100 leaderboard API calls per client per minute is a typical cache counter: 1. Build a key from the client and the current minute, for example `ratelimit:leaderboard:<client id>:<minute>`. 2. `cache.add(key, 0, 60)` so the counter exists and expires with the minute. 3. `count = cache.incr(key)`; if `count` exceeds 100, reject the request. On Redis or Memcached this is atomic and the counter expires on schedule. On `DatabaseCache` two things break: concurrent increments can be lost, letting clients exceed the limit, and each increment resets the timeout to the alias default. On `LocMemCache` each worker process keeps its own counter, so the real limit is 100 times the number of processes. ## When not to use cache counters Caches can lose data: eviction under memory pressure, a restart, `LocMemCache` being per process. A cache counter is fine for approximate numbers, like "views in the last hour" or a soft rate limit. Anything that must be exact, such as a player's points, belongs in the database, updated with an `F()` expression inside the write that changes it.
- Why does RedisCache store integers without pickling them?So that Redis itself can do the arithmetic. Redis's increment commands only work on values it can parse as integers; a pickled byte string would not qualify. Leaving plain `int` values unpickled lets `cache.incr()` map to one atomic server command.
- A counter on DatabaseCache was created with a one-day timeout but disappears after about five minutes. Why?`DatabaseCache` uses the base `incr()`, which reads the value and writes it back with `set()` and no timeout, so each increment resets the entry to the alias's default `TIMEOUT` of 300 seconds. Pass the timeout again with `touch()` after incrementing, or use a backend with a native increment.
saying these in an interview costs you the question
- cache.incr() creates the key with value 1 if it is missing.
- cache.incr() is atomic on every Django cache backend.
- LocMemCache increments are atomic across worker processes.
- Incrementing never changes a key's expiry on any backend.
- Cache counters are a safe place for balances or scores.