A Django shop on several load-balanced servers sets SESSION_ENGINE to the cache engine without configuring CACHES, and baskets randomly empty; why, and what do you change?
answer
- what backs the cache by default
- one process, one memory
- a request lands elsewhere
- database first, cache second
basics
~10 sWith no CACHES configured, Django's cache session engine uses the default LocMemCache, which lives inside one process, so requests landing on another worker see no session. Use cached_db, or a shared cache backend, instead.
solid answer
~40 sThe `cache` engine stores sessions only in the cache named by `SESSION_CACHE_ALIAS` (default `"default"`), and an unconfigured `CACHES` gives that alias `LocMemCache`, a per-process memory store. Each worker on each server has its own copy, so a request routed to a different process finds no session and starts a new one; recycling a worker wipes its sessions too. Nothing errors, which is why it looks random. Fix it by giving every process one store: `cached_db`, which writes the database first and falls back to it after cache eviction, is the safe choice for baskets and logins; `cache` on a shared Redis or Memcached backend is acceptable only for disposable data. Consider a dedicated `SESSION_CACHE_ALIAS`, schedule `clearsessions` once sessions are in the database, and do not paper over it with sticky load balancing.
code
python · 14 lines# settings.py - one session store for every worker on every server
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
SESSION_CACHE_ALIAS = "sessions"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://cache.internal:6379/0",
},
"sessions": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://sessions-cache.internal:6379/0",
},
}go deeper
Remember that Django's default cache is local memory in one process, so it cannot share sessions across workers or servers.
Explain how the cache engine uses SESSION_CACHE_ALIAS, why LocMemCache is per-process, and how cached_db's write order and fallback differ.
Diagnose silent session loss across a fleet, pick cached_db or a shared cache by data value, isolate sessions from page-cache eviction, and verify across workers.
Decide whether session state should live in a shared tier at all, and what a cache outage may cost in re-logins before you accept a cache-only engine.
## The symptom A Django shop runs four app servers, each with several worker processes, behind a round-robin load balancer. Settings contain: ```python SESSION_ENGINE = "django.contrib.sessions.backends.cache" # CACHES not configured ``` Customers report baskets that empty themselves, and logged-in users are bounced to the login page on some clicks but not others. Nothing appears in the error logs. ## The cause: the default cache is per-process With no `CACHES` setting, Django uses its default, a single `"default"` alias backed by `django.core.cache.backends.locmem.LocMemCache`. The **local-memory cache** lives inside one Python process: - Each worker process on each server has its own private copy. - A request that lands on a different process looks up the session key in an empty cache, finds nothing, and the `cache` engine treats the session as new. - Restarting or recycling a worker wipes its sessions. The `cache` session engine keeps data **only** in the cache (`SESSION_CACHE_ALIAS`, default `"default"`), so there is no second place to recover from. The Django docs are direct about it: use cache-based sessions only with the Memcached or Redis backends, because local-memory is not multi-process safe and does not retain data long enough. ## What the cache engine does on a miss The silence comes from how the engine loads a session. `SessionStore.load()` in the `cache` backend looks up the cache key built from the cookie's session key. When the lookup returns nothing, it sets the session key to `None` and returns an empty dict. From Django's point of view this is simply a visitor without a session: when the view adds an item, a **new** key is generated, saved into this process's cache, and sent as a new `sessionid` cookie. The next request may land on the first process again, which does not know the new key either. No exception is raised anywhere, so the logs stay clean while users bounce between empty baskets. ## Other engine choices that break the same way | Setup | Why a fleet loses sessions | |---|---| | `cache` + `LocMemCache` | Data lives in one process | | `cache` + `FileBasedCache` or `file` engine on local disk | Data lives on one host's disk | | `cache` + a shared cache that evicts under memory pressure | Evicted sessions are simply gone | | `db` or `cached_db` against a shared database | Works across the fleet | | `signed_cookies` | Works across the fleet; data travels with the browser | ## The fix 1. **Point every process at the same store.** Either configure a shared cache in `CACHES` (Django ships Redis and Memcached backends), or move sessions to the database. 2. **Prefer `cached_db` for anything you cannot afford to lose**, such as a basket or a login. It writes the database first and the cache second, and reads fall back to the database after an eviction, so a cache restart costs latency rather than data. 3. **Keep `cache` only for disposable data**, and then on a shared, adequately sized cache. 4. **Set `SESSION_CACHE_ALIAS`** to a dedicated alias backed by its own cache instance if the default cache also holds pages; sharing one instance lets heavy page caching evict sessions. 5. **Schedule `manage.py clearsessions`** once the database holds sessions, because the `db` and `cached_db` engines leave expired rows until it runs. A tempting non-fix is **sticky sessions** at the load balancer: it hides the problem until a worker restarts or a node is replaced. Another is `signed_cookies` for a basket that may grow; it works across the fleet but brings a per-cookie size limit and no server-side revocation for the login that shares the session. ## Verifying - In the Django shell on two different servers, `from django.core.cache import cache; cache.set("probe", 1)` on one and `cache.get("probe")` on the other must agree. - With `cached_db`, deleting a session's cache key and reloading a page must keep the basket. - Load tests should hit multiple workers per user, not one. ## Why this is asked The question checks that a candidate knows the defaults behind a one-line setting change: `SESSION_ENGINE` points at a cache, the cache defaults to `LocMemCache`, and the local-memory backend is per-process. Knowing `cached_db` exists, and why its write order and fallback matter, separates someone who has run Django on more than one machine from someone who has not.
- Why is enabling sticky sessions on the load balancer not a real fix for a Django project using LocMemCache sessions?It keeps a user on one server but not necessarily one worker process, and it loses every session held by a worker when that worker restarts, recycles or its node is replaced. The data still lives in one process's memory; stickiness only lowers how often you notice.
- Would switching this Django shop to the signed_cookies engine fix the vanishing baskets?Yes for sharing, since the data travels with the browser and any worker can verify it. But the basket must stay within the browser's per-cookie size limit, its contents become readable by the user, and the same cookie then carries the login, which cannot be revoked server-side on logout.
Like a cloakroom where each attendant keeps tickets in their own pocket: hand your ticket to a different attendant and your coat does not exist.
saying these in an interview costs you the question
- Django's default cache is shared between worker processes
- The cache session engine falls back to the database on a miss
- Sticky load balancing fully fixes per-process session storage
- cached_db writes the cache first and the database later
- Losing a session raises an error you would see in the logs