skip to content

Why is Django's default MESSAGE_STORAGE FallbackStorage, and what goes wrong if you use CookieStorage or SessionStorage alone?

level: seniorimportance: should knowfreq 28%

answer

  1. cookie first, then overflow
  2. a two-kilobyte ceiling
  3. every notice writes the session
  4. DEBUG turns loss into an error

basics

~20 s

Django's FallbackStorage keeps short notices in a signed messages cookie and spills overflow into the session, so the common case needs no session write. CookieStorage alone drops the oldest messages past 2048 bytes; SessionStorage alone saves the session for every notice.

solid answer

~40 s

`MESSAGE_STORAGE` defaults to `FallbackStorage`, which tries `CookieStorage` first and passes whatever does not fit to `SessionStorage`. `CookieStorage` writes a signed, readable cookie named `messages`, capped at `max_cookie_size = 2048` bytes, and reuses the `SESSION_COOKIE_*` flags; alone, it drops the oldest messages that do not fit — `MessageMiddleware` raises `ValueError("Not all temporary messages could be stored.")` under `DEBUG`, and in production they are silently lost. `SessionStorage` stores the list under `request.session["_messages"]`, needs the session middleware before `MessageMiddleware` or raises `ImproperlyConfigured`, and modifies the session on every notice — creating a session for anonymous visitors. Fallback gives the cheap cookie path for one short notice and still delivers bursts. Choose `SessionStorage` when message text must not be client-readable.

code

python · 6 lines
python
# settings.py
# Default; shown for review:
MESSAGE_STORAGE = "django.contrib.messages.storage.fallback.FallbackStorage"

# Keep notice text out of the browser (every user already has a session):
# MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"

go deeper

for a junior

Know that messages are stored between requests in a cookie, the session, or both, and that the default tries the cookie first.

for a middle

Describe the three storage classes, the 2048-byte cookie cap and the _messages session key, and what each needs installed.

for a senior

Reason about silent loss with CookieStorage in production, session writes and anonymous sessions with SessionStorage, and readable text in cookies.

for a principal

Treat notice storage as part of the session footprint: decide whether anonymous traffic may create sessions and what client-visible data is acceptable.

## Where Django keeps a message between requests A message added with `messages.success(request, "Profile saved.")` must survive the redirect to the next request. The `MESSAGE_STORAGE` setting picks the class that stores it; the default is `django.contrib.messages.storage.fallback.FallbackStorage`. Django ships three: | Storage | Where messages live | Size limit | Needs sessions | |---|---|---|---| | `cookie.CookieStorage` | A signed cookie named `messages` | 2048 bytes of cookie value (`max_cookie_size`) | No | | `session.SessionStorage` | The session, under the key `_messages` | Whatever the session engine allows | **Yes** | | `fallback.FallbackStorage` (**default**) | Cookie first, overflow in the session | None in practice | **Yes** installed; written only on overflow | ## CookieStorage alone - The cookie is **signed** with Django's cookie signer (derived from `SECRET_KEY`, with a fixed salt), so tampering is detected, but the text is **readable** by the client. - It reuses the session cookie flags: `SESSION_COOKIE_DOMAIN`, `SESSION_COOKIE_SECURE`, `SESSION_COOKIE_HTTPONLY` and `SESSION_COOKIE_SAMESITE`. - When the encoded messages exceed `max_cookie_size`, it keeps the newest messages that fit and **drops the oldest**, adding a marker that says not everything fit. The dropped messages are returned to the middleware as "unstored": with `DEBUG = True` `MessageMiddleware` raises `ValueError("Not all temporary messages could be stored.")`; in production they are silently lost. - It needs no session, so anonymous visitors cost nothing server-side. ## SessionStorage alone - It stores the serialized list in `request.session["_messages"]`, so it can hold many or long messages. - It requires `request.session`; if the session middleware is missing or runs after `MessageMiddleware`, creating the storage raises `ImproperlyConfigured`. - Every notice **modifies the session**, which means a session save and a session cookie. For an anonymous visitor that means creating a session (a database row with the default `db` engine) just to say "Thanks for subscribing." - It inherits the session engine's properties: with the `signed_cookies` engine the messages end up in a cookie anyway. ## Why FallbackStorage is the default `FallbackStorage` tries `CookieStorage` first and passes anything that did not fit to `SessionStorage`. It builds both storages for every request, so it needs the session middleware installed even though it writes to the session only on overflow: 1. On save, the cookie storage keeps the **oldest** messages that fit (it is called with `remove_oldest=False`), and the newer overflow goes to the session. 2. On read, it reads the cookie; only if the cookie says not everything fit does it also read the session. 3. A storage that held messages last time is cleared when they are consumed, even if nothing new goes into it. The common case — one short notice after a redirect — therefore costs a small cookie and **no session write**, and the rare burst of long messages still gets delivered instead of dropped. ## Choosing - Keep the default unless you have a reason. - Pick `SessionStorage` if message text must not be readable client-side, or if messages are long and frequent, and every user already has a session anyway. - Pick `CookieStorage` only when sessions are not installed, and keep messages short and few. - Whatever you pick, messages are consumed the same way: when a template iterates `messages`. ## Where to look when a notice goes missing - **CookieStorage**: inspect the `messages` cookie on the redirect response; if it carries the not-finished marker, older notices were trimmed, and in production nothing reports it. - **SessionStorage**: check `request.session["_messages"]` between requests, and confirm the session was actually saved (a 5xx response skips the session save). - **FallbackStorage**: check both, in that order; the session holds only what overflowed from the cookie. - **Any storage**: confirm the landing page's template iterates `messages`, since storage only decides where a notice waits, not when it is shown. ## Operational notes - Message text is marked safe again on load only if it was a `SafeData` string when stored; plain strings are escaped as usual in templates. - The behaviour for parallel requests from the same client is undefined for cookie- and session-backed storage, per the Django docs. - `MessageMiddleware` must come after `SessionMiddleware` for any storage that touches the session, including the default.

  • When Django's FallbackStorage overflows, which messages stay in the cookie and which go to the session?
    The fallback calls the cookie storage with `remove_oldest=False`, so the cookie keeps the oldest messages that fit and adds a marker saying more exist; the newer overflow goes to `SessionStorage`. On read, the session is consulted only when the cookie carries that marker, so a normal request never touches the session.
  • Can a user read or change the text of a Django message stored by CookieStorage?
    Read, yes: the cookie is signed, not encrypted. Change, no: it is signed with Django's cookie signer and a fixed salt, and on a bad signature the storage discards the data and marks it used so the cookie is removed. Keep sensitive details out of message text, or use `SessionStorage`.

saying these in an interview costs you the question

  • The default message storage is SessionStorage
  • CookieStorage encrypts the message text
  • Messages that do not fit in the cookie always raise an error
  • FallbackStorage writes every message to both the cookie and the session
  • SessionStorage works without SessionMiddleware