Django Messages Framework: Flash Messages Guide

Blog / Django · December 4, 2020 · Updated June 10, 2026 · 6 min read
Django Messages Framework: Flash Messages Guide

Almost every web app needs to tell the user what just happened: "Profile saved", "Invalid login", "Your trial ends in 3 days". Django ships a small, batteries-included tool for exactly this — the messages framework. It stores short, one-time flash messages on one request and shows them on the next, then clears them automatically.

This guide is current for Django 5.x and covers the full API: adding messages, levels, storage backends, mapping levels to CSS classes, displaying messages in templates and class-based views, plus a quick note on HTMX. If you are new to Django templating, the Django template engine basics make a good companion read.

What is the messages framework?

The messages framework solves the post/redirect/get (PRG) problem. After a successful POST you normally redirect so the user can't resubmit by refreshing — but a plain redirect throws away any context about what happened. Messages bridge that gap: you attach a message during the POST, redirect, and the message survives into the next GET response, where the template renders it once and discards it.

Each message has:

  • text — what the user sees.
  • a levelDEBUG, INFO, SUCCESS, WARNING or ERROR.
  • tags — space-separated, CSS-friendly strings derived from the level plus any extra_tags you pass.

Default setup

When you run django-admin startproject, the framework is already wired up — you rarely touch settings. For reference, these are the three pieces that must be present:

  1. django.contrib.messages in INSTALLED_APPS.
  2. django.contrib.sessions.middleware.SessionMiddleware and django.contrib.messages.middleware.MessageMiddleware in MIDDLEWARE.
  3. django.contrib.messages.context_processors.messages in the context_processors of your TEMPLATES backend — this is what exposes messages to every template.
# settings.py
INSTALLED_APPS = [
    # ...
    "django.contrib.messages",
]

MIDDLEWARE = [
    # ...
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
]

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "OPTIONS": {
            "context_processors": [
                # ...
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

Adding messages in a view

The quickest way is the level-named shortcuts. Each takes the request and the message text:

from django.contrib import messages

def my_view(request):
    messages.debug(request, "%s SQL statements were executed." % count)
    messages.info(request, "Three credits remain in your account.")
    messages.success(request, "Profile details updated.")
    messages.warning(request, "Your account expires in three days.")
    messages.error(request, "Document deleted.")

Under the hood every shortcut calls add_message(), which you can also use directly — handy for a dynamic level or a custom one:

from django.contrib import messages

# add_message(request, level, message, extra_tags="", fail_silently=False)
messages.add_message(request, messages.SUCCESS, "Order placed.", extra_tags="toast")

Two optional arguments matter:

  • extra_tags — extra space-separated strings appended to the message's tags. Use them as CSS hooks (e.g. "toast", "dismissible").
  • fail_silently=True — don't raise MessageFailure if the middleware/storage isn't configured. Useful when adding messages from code that may run outside a normal request, such as management commands or tests.

Message levels and MESSAGE_LEVEL

Levels are just integers, so they can be filtered. Anything below MESSAGE_LEVEL is silently dropped. The default is INFO (20), which is why messages.debug() produces nothing in a fresh project until you lower the threshold.

# Built-in levels (django.contrib.messages.constants)
DEBUG    = 10
INFO     = 20
SUCCESS  = 25
WARNING  = 30
ERROR    = 40

# settings.py - show DEBUG messages in development
from django.contrib.messages import constants as message_constants
MESSAGE_LEVEL = message_constants.DEBUG  # or just the integer 10

Storage backends

Where are messages kept between the two requests? That's the storage backend, chosen with MESSAGE_STORAGE.

  • FallbackStorage (default) — writes to a signed cookie first and automatically falls back to the session for messages too large to fit in the ~4 KB cookie. Best of both worlds; leave it alone unless you have a reason.
  • CookieStorage — cookie only. No server-side state, but limited size and the data rides on every request.
  • SessionStorage — session only. No size limit, but requires django.contrib.sessions and server-side session storage.

Switch backends with a single setting:

# settings.py
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"
# Default is:    "django.contrib.messages.storage.fallback.FallbackStorage"

Mapping levels to CSS classes with MESSAGE_TAGS

By default each level renders a matching tag string (error, success, ...). CSS frameworks often expect different names — Bootstrap and many Tailwind setups use danger for errors. Remap level → tag once in settings and your templates stay clean:

# settings.py
from django.contrib.messages import constants as messages

MESSAGE_TAGS = {
    messages.DEBUG: "secondary",
    messages.INFO: "info",
    messages.SUCCESS: "success",
    messages.WARNING: "warning",
    messages.ERROR: "danger",   # was "error"
}

Displaying messages in templates

The context processor exposes a messages iterable to every template. Loop over it once — iterating marks the messages as read so they won't reappear. Each message stringifies to its text and carries message.tags and message.level.

Here is a reusable Tailwind block; drop it in your base layout so every page shows flash messages. The in test keeps it robust even when extra_tags are present:

{% if messages %}
  <div class="space-y-2">
    {% for message in messages %}
      <div class="rounded-md px-4 py-3 text-sm
        {% if 'success' in message.tags %}bg-green-100 text-green-800
        {% elif 'danger' in message.tags or 'error' in message.tags %}bg-red-100 text-red-800
        {% elif 'warning' in message.tags %}bg-yellow-100 text-yellow-800
        {% else %}bg-blue-100 text-blue-800{% endif %}">
        {{ message }}
      </div>
    {% endfor %}
  </div>
{% endif %}

If you prefer numeric comparisons over tag strings, the context also provides DEFAULT_MESSAGE_LEVELS, e.g. {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}.

Using messages with class-based views

With function-based views you add messages inline. For class-based views, the framework ships SuccessMessageMixin, which adds a message automatically when a form validates (build the form itself with Django's forms API). You can also drop down to messages.success() inside form_valid() for full control. Note the redirect — the message is added before the HttpResponseRedirect, then rendered after the browser follows it (the PRG pattern):

from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView, UpdateView

from myapp.forms import StudentForm
from myapp.models import Student


class StudentCreateView(SuccessMessageMixin, CreateView):
    model = Student
    form_class = StudentForm
    success_url = reverse_lazy("student_list")
    # %(name)s pulls from the submitted form's cleaned data
    success_message = "%(name)s was created successfully."


class StudentUpdateView(UpdateView):
    model = Student
    form_class = StudentForm
    success_url = reverse_lazy("student_list")

    def form_valid(self, form):
        response = super().form_valid(form)
        messages.success(self.request, f"{self.object.name} was updated.")
        return response

Rendering messages in HTMX / AJAX flows

With HTMX a partial response replaces only a fragment of the page, so a global messages block in the base template never re-renders. The clean fix is an out-of-band (OOB) swap: include the messages partial in the response and mark it hx-swap-oob="true", so HTMX patches it into a fixed container wherever that lives on the page.

Keep a <div id="messages"></div> in your base layout, then return this partial alongside your main fragment:

<!-- messages.html -->
<div id="messages" hx-swap-oob="true">
  {% for message in messages %}
    <div class="rounded-md px-4 py-3 text-sm {{ message.tags }}">{{ message }}</div>
  {% endfor %}
</div>

Because iterating messages consumes them, the partial both displays the current flash messages and clears the storage in the same response — exactly the behaviour you want for an AJAX update. For deeper template work, see custom template tags and filters.

Need a hand wiring notifications, forms or HTMX into a production Django app? Our Django development services team can help.

Frequently Asked Questions

Why aren't my messages showing up?

The usual causes are: the messages context processor is missing from TEMPLATES; your view added a message but re-rendered instead of redirecting, so the next request never ran; or something earlier in the page already iterated messages and consumed them. Also confirm MESSAGE_LEVEL isn't filtering out the level you used.

Do messages persist across a redirect?

Yes — that's the whole point. A message added during a POST is stored, survives the redirect, and is rendered on the following GET, then cleared. This is the post/redirect/get pattern.

How long do messages last?

They are one-time flash messages. They live until the first response that iterates over them, then they are deleted. They are not a persistent notification system; for an inbox-style feed, store notifications in your own model.

What's the difference between the storage backends?

FallbackStorage (the default) uses a signed cookie and spills over to the session when messages exceed the ~4 KB cookie limit. CookieStorage is cookie-only (no server state, size-limited). SessionStorage is session-only (no size limit, needs server-side sessions). Most apps should keep the default.

How do I change the CSS class for errors (for example to Bootstrap's danger)?

Set MESSAGE_TAGS in settings to remap a level to a different tag string, e.g. {messages.ERROR: "danger"}. The new string then appears in message.tags in your template.

Can I add messages outside a view?

You can call messages.add_message() anywhere you have access to the request. In code paths that may run without configured message storage (management commands, some tests), pass fail_silently=True so it won't raise MessageFailure.

Share this article