skip to content

What can Django's signed_cookies session engine not do that the database engine can, and when is it still a reasonable choice?

level: seniorimportance: should knowfreq 34%

answer

  1. no record on the server
  2. logout clears one browser
  3. signed is not encrypted
  4. size rides every request

basics

~20 s

Django's signed_cookies engine keeps the whole session in a signed, unencrypted cookie, so it cannot revoke a session server-side, cannot hide data from the user and is capped by cookie size; it suits small, non-sensitive state.

solid answer

~40 s

With `signed_cookies` the serialized session is compressed and signed with `SECRET_KEY` and becomes the cookie value; there is no server-side record. So logout's `flush()` only clears the cookie in that browser — a copied cookie stays valid until it is older than `SESSION_COOKIE_AGE`, and replay is possible because a signature proves origin, not freshness. The data is readable by the user, the browser's per-cookie size limit caps it, and it travels on every request. Your levers are coarse: a password change breaks the auth session hash, rotating `SECRET_KEY` kills every cookie, and a short `SESSION_COOKIE_AGE` bounds the damage. `clearsessions` has nothing to do. It is reasonable for tiny, non-sensitive, tamper-evident state on a stateless fleet, not for logins that must be killable.

code

python · 5 lines
python
# settings.py - acceptable only for small, non-sensitive session data
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
SESSION_COOKIE_HTTPONLY = True   # default; keeps page scripts from reading it
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_AGE = 60 * 60 * 4  # bounds how long a copied cookie keeps working

go deeper

for a junior

Know that signed_cookies keeps the session in the browser, signed but readable, and that logout cannot kill copies of it.

for a middle

Explain how the engine signs and compresses the serialized dict, why flush() only affects one browser, and how SESSION_COOKIE_AGE bounds acceptance.

for a senior

Argue when revocation matters, list the coarse levers (password change, SECRET_KEY rotation, short age), and keep sensitive or growing data out of the cookie.

for a principal

Trade stateless scaling against losing server-side control of sessions; for authenticated products the default should be a server-side engine.

## How the signed_cookies engine works With `SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"`, Django keeps **no server-side session record**. On save, the engine serializes the whole session dict (with `SESSION_SERIALIZER`, JSON by default), compresses it and signs it with `django.core.signing` using `SECRET_KEY` and a fixed salt. That signed string **is** the session key, and it goes into the cookie named by `SESSION_COOKIE_NAME`. On load, the engine verifies the signature and an age limit of `SESSION_COOKIE_AGE`; any failure resets the session to empty. It is attractive for stateless fleets: no database table, no cache, every app server can read every session. ## What it cannot do | Capability | `db` / `cached_db` | `signed_cookies` | |---|---|---| | Revoke one session server-side | Yes, delete the record | **No**, there is no record | | Invalidate on logout everywhere | Yes, `flush()` deletes the row | **Only this browser's cookie is cleared** | | Keep data secret from the user | Yes, only the key is sent | **No**, signed but not encrypted | | Hold a large session | Limited by the store | Limited by the browser's per-cookie size (commonly 4096 bytes) | | Clean up expired sessions | `clearsessions` deletes rows | Nothing to clean; `clear_expired()` is a no-op | ## Why logout does not end a stolen session On logout Django calls `request.session.flush()`. For a server-side engine that deletes the stored record, so every copy of the old cookie now points at nothing. For `signed_cookies`, `delete()` just empties the session in memory, and the middleware then deletes the cookie in **that browser** (or replaces it if the view stores new data). A copy of the old cookie captured earlier is still correctly signed and still younger than `SESSION_COOKIE_AGE`, so it still loads. The Django docs state this plainly: cookie-based sessions are not invalidated when a user logs out, and a stolen cookie keeps working until it is older than `SESSION_COOKIE_AGE`. The same property allows **replay**: the signature proves the data came from your site, not that it is the latest version you issued. ## The levers you do have - **Password change.** `django.contrib.auth` stores a session auth hash derived from the user's password hash in the session. When the user's password changes, `get_user()` finds a mismatch and flushes the session, so stolen cookies stop authenticating that user. Non-auth data in the stolen cookie remains readable. - **Rotating `SECRET_KEY`.** Invalidates every signed cookie at once, for all users, unless the old key stays in `SECRET_KEY_FALLBACKS`. A blunt instrument. - **A short `SESSION_COOKIE_AGE`.** Bounds how long a stolen cookie works, at the cost of more frequent logins. - **Keep secrets out.** Anything in the session is readable by the user, so no prices a user must not tamper with (tampering is detected, reading is not prevented), no internal flags, no personal data. ## Other operational traps 1. **Size.** Django compresses the payload, but a growing basket or a list of recent items can exceed the browser's cookie limit; the browser may then drop the cookie, and the session silently resets. 2. **Bandwidth.** The whole session travels on every request to the cookie's path. 3. **`SESSION_SAVE_EVERY_REQUEST`** means re-signing and re-sending the cookie on every response. 4. **Custom expiry.** The engine checks age against `SESSION_COOKIE_AGE`; the source notes that a non-default expiry set with `set_expiry()` is not reflected in that server-side age check. 5. **`SESSION_COOKIE_HTTPONLY`** should stay `True` (its default) so page scripts cannot read the data. ## Checklist before choosing it - Is every value in the session small, bounded and safe for the user to read? - Is it acceptable that logout cannot kill copies of the cookie? - Is `SESSION_COOKIE_AGE` short enough to bound a stolen cookie's life? - Do login sessions share this cookie? If so, revocation requirements usually decide against it. ## When it is the right choice Small, non-sensitive, tamper-evident state where server-side revocation is not a requirement: a UI preference, a short wizard step, an anonymous visitor's locale. For authenticated sessions where "log out everywhere" or "kill this session" must work, a server-side engine such as `cached_db` is the defensible default.

  • With Django's signed_cookies engine, does changing a user's password invalidate a stolen session cookie?
    For authentication, yes. `django.contrib.auth` stores a session auth hash derived from the password hash; after a password change `get_user()` sees a mismatch and flushes the session, so the stolen cookie no longer logs the attacker in. Any non-auth data in that cookie is still readable by whoever holds it.
  • What happens to every Django session when SECRET_KEY is rotated without SECRET_KEY_FALLBACKS?
    Signatures made with the old key stop verifying. For `signed_cookies` every cookie fails and the session resets to empty; database-backed sessions are signed too, so their stored data also becomes unreadable. Listing the old key in `SECRET_KEY_FALLBACKS` lets existing sessions keep working while new ones are signed with the new key.

saying these in an interview costs you the question

  • Signed cookie sessions are encrypted, so users cannot read them
  • Logging out invalidates every copy of a signed session cookie
  • clearsessions must run to expire signed cookie sessions
  • A valid signature proves the cookie is the latest one issued
  • Signed cookies can hold arbitrarily large sessions because Django compresses them