In Django's messages framework, what exactly consumes a message, and why can a notice survive redirects or appear on a later page?
answer
- looking is not reading
- a used flag on the storage
- three outcomes at response time
- the landing page never loops
basics
~20 sIn Django's messages framework only iterating the storage marks messages used; at the response, MessageMiddleware drops used messages and keeps unread ones, so a notice survives redirects and waits for the next page that loops over messages.
solid answer
~40 s`messages` in a template is the request's storage object. `BaseStorage.__iter__` sets `used = True`; `len()`, `{% if messages %}` and `in` do not. At the end of the request `MessageMiddleware` calls `storage.update(response)`: if the storage was iterated it stores only messages queued after iteration; if not iterated but something was added it stores old plus new; otherwise it leaves storage untouched. Redirect responses iterate nothing, so notices survive any number of redirects. A notice appears on a later page when the landing page never loops over `messages` — a template not extending the base, a JSON response — and vanishes unseen when some code iterates `get_messages(request)` without rendering it. Setting `storage.used = False` after iterating keeps them.
code
python · 19 linesimport logging
from django.contrib import messages
logger = logging.getLogger(__name__)
def log_pending_notices(get_response):
def middleware(request):
response = get_response(request)
storage = messages.get_messages(request)
if not isinstance(storage, list): # [] when MessageMiddleware is absent
already_used = storage.used
for message in storage: # iterating sets storage.used = True
logger.debug("notice %s: %s", message.level_tag, message)
storage.used = already_used # keep unread notices, drop displayed ones
return response
return middlewarego deeper
Remember that only looping over messages consumes them; checking whether any exist does not.
Explain the used and added_new flags and the three outcomes of update() at response time, and why redirects preserve messages.
Diagnose late or vanished notices by finding which request first iterated the storage, including middleware and JSON views, and use storage.used = False deliberately.
Define where notices are rendered for mixed server-rendered and script-driven pages so the consuming request is always the one the user sees.
## The storage object behind `messages` In a Django template, `messages` is not a list. It is the request's **message storage** object (a `BaseStorage` subclass, `FallbackStorage` by default), created by `MessageMiddleware.process_request` and exposed by the `messages` context processor. It holds two groups: - **loaded messages** — read lazily from the cookie or session the first time you look at them; - **queued messages** — added during this request with `messages.add_message()` or a shortcut such as `messages.success()`. It also keeps two flags: `used` and `added_new`. ## What consumes a message Only **iteration** does. `BaseStorage.__iter__` sets `used = True`, moves queued messages into the loaded list, and returns an iterator. Other operations do not consume: | Operation | Loads stored messages | Marks them used | |---|---|---| | `{% for message in messages %}` / `for m in get_messages(request)` | Yes | **Yes** | | `{% if messages %}` / `len(storage)` | Yes | No | | `message in storage` | Yes | No | | Never touching `messages` | No | No | ## What the middleware writes back At the end of the request, `MessageMiddleware.process_response` calls `storage.update(response)`, which decides what survives: 1. **Iterated** (`used` is true): store only the messages queued **after** the last iteration. Everything shown is dropped. 2. **Not iterated, but something new was added** (`added_new`): store the previously loaded messages **plus** the new ones. 3. **Neither**: do nothing, so whatever is stored stays exactly as it was. ## Consequences that explain real bugs - **Notices survive redirects.** A redirect response renders no template and nothing iterates the storage, so rule 2 or rule 3 keeps the message. A chain of several redirects is fine. - **A notice shows up on a later, unrelated page.** The page the user lands on does not iterate `messages` — its template does not extend the base template that holds the loop, or it only uses `{% if messages %}`, or the response is JSON for a script. Rule 3 keeps the notice until the next page that does iterate. - **A notice disappears without being seen.** Something iterated the storage without displaying it: a view that loops over `get_messages(request)` for logging, or a template loop inside markup that is hidden. Rule 1 then drops it. - **A message added after the loop** (for example in a template tag that runs later, or in middleware) is kept for the next request, because rule 1 stores messages queued after iteration. - **Parallel requests.** The Django docs call behaviour undefined when the same client sets and reads messages in parallel, for example in two tabs: a notice can appear in the other tab. ## Keeping messages after iterating The documented escape hatch is to reset the flag: ```python from django.contrib import messages storage = messages.get_messages(request) for message in storage: audit_log(message) storage.used = False # keep them for the template ``` If a template may already have iterated the storage in the same request, save the previous value of `used` and restore it instead of forcing `False`; otherwise notices the user already saw are stored again and shown twice. ## Diagnosing it - Check that the landing template actually runs a `{% for message in messages %}` loop, not only `{% if messages %}`. - Search views, middleware and template tags for iteration over `get_messages(request)`. - Inspect the `messages` cookie or the `_messages` session key between requests to see what was stored. - In tests, `MessagesTestMixin.assertMessages(response, expected)` (Django 5.0+) checks what a response added. ## Summary Messages are consumed by iteration, not by existence checks, and the middleware stores what is left at the end of each request. Every "wrong page" or "vanished notice" bug comes back to which request first iterated the storage.
- Does a Django redirect chain of three hops lose a message added before the first redirect?No. Redirect responses render no template, so nothing iterates the storage. On the first response the middleware stores the new message; on the next two the storage is neither used nor given new messages, so it is left untouched. The message waits until a page iterates `messages`.
- In Django, what happens to a message added after a template has already iterated messages in the same request?It is kept for the next request. When the storage has been iterated, `update()` stores only messages queued after the iteration, so a message added later — by a template tag that runs afterwards or by middleware on the way out — shows on the following page.
Like a note left on the fridge: glancing to see whether there is a note leaves it there, but reading it means it goes in the bin.
saying these in an interview costs you the question
- {% if messages %} clears the messages
- Messages expire automatically after the next request
- A redirect response consumes the message
- Iterating get_messages() in Python code has no effect on what templates see
- Messages are deleted as soon as they are added to the page context