django-maintenance-mode: a 503 page that does not need the database to be up
:construction: :hammer_and_wrench: shows a 503 error page when maintenance-mode is on.
At a glance
- What is it?
- django-maintenance-mode shows a 503 error page when maintenance mode is on, and the design decision that shapes everything is that it runs at application level inside Django rather than in the web server, so the Django process has to be alive. That also means it does not use the database and does not prevent database access, and it gives you four different state backends plus per-audience exemptions so staff, superusers, anonymous users and specific IP addresses can still get through.
- Who is it for?
- Adopt django-maintenance-mode if your Django site needs to be closed to ordinary users while it stays running, for a migration, a data import or a deliberate freeze, since the state can live in a file and does not need the database. Do not adopt it as your disaster recovery plan, because it runs inside the application, so when the process is down or the code will not boot the page never appears and you need a server-level maintenance response instead.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What it does, and the two limitations that define it
django-maintenance-mode shows a 503 error page when maintenance mode is on.
The two lines after that are the whole design in brief. It works at application level, so your Django instance should be up. And it doesn't use the database and doesn't prevent database access.
Those are not caveats, they are the concept. A maintenance page implemented as a middleware inside Django is running in the same process as your application, which means it is available exactly when your application is available. It cannot be used for the case where the whole application is down, the database is unreachable, or a deploy has left the code in a state that cannot boot, because in all of those cases the middleware never runs.
What it is for is the case where the application is up and healthy but you do not want anybody using it. A database migration in progress, a long-running data import, a deliberate freeze while you fix something. In that window, showing a 503 to ordinary users and letting your staff through is exactly the right behaviour, and doing it from a file or a cache rather than the database is a feature, because the database is the thing you are trying not to depend on.
The primary language is Python, the licence is MIT, the default branch is main, and the recent releases are 0.23.0 from 2026-07-14, 0.22.0 from 2025-04-03 and 0.21.1 from 2024-01-24. The last push was on 2026-10-20 with the project not archived.
The supported Django versions are worth reading as a compatibility signal, because the list is what a current Django project needs:
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6.0",
"Framework :: Django :: 6.1",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Software Development :: Build Tools",
]So Django 4.2 through 6.1, and Python 3.11 through 3.14. The runtime dependencies are correspondingly small:
dependencies = [
"django >= 4.2",
"python-fsutil >= 0.17.0, < 1.0.0",
]Django itself and python-fsutil, which is the filesystem helper the local file backend uses. There is no dependency on a cache library or a database driver, which is consistent with a package whose whole point is to keep working when those things are not available.
Installing it, and the middleware ordering trap
Installation is five steps, and the third one has a note attached that is the most useful thing in the README.
Run pip install django-maintenance-mode or download it and add the maintenance_mode package to your project. Add maintenance_mode to settings.INSTALLED_APPS before custom applications. Add maintenance_mode.middleware.MaintenanceModeMiddleware to settings.MIDDLEWARE as the last middleware. Add your custom templates/503.html file. And restart your application server.
Three of those steps have reasons that are not obvious from the text alone.
The INSTALLED_APPS position, before custom applications, is because the middleware and the 503 template rendering need the app's templates to be findable before anything else processes the request.
The templates/503.html file is not optional, and the reason is in the topic list: service-unavailable. The status code is 503, and a 503 response needs a body, and Django's default 503 handler is deliberately minimal because it is meant for situations where things are broken. Supplying your own template is how you show a branded page, a contact address and an estimate of when the service will be back.
The middleware position note is the interesting one. By default the middleware runs last, so preceding middleware such as session and auth still executes during maintenance mode. To skip that, for example during database migrations or automated deployments, you move MaintenanceModeMiddleware higher up and use MAINTENANCE_MODE_IGNORE_URLS to keep specific URLs accessible.
And the note carries a warning: skipping middleware also disables its features, for example request.user, so the user-based settings MAINTENANCE_MODE_IGNORE_*USER will not work.
That trade is stated rather than hidden. Putting the middleware last is what makes the user-based exemptions work, because the authentication middleware has already populated request.user by the time it runs. Moving it earlier makes the page cheaper and lets it survive a database outage, but you lose the ability to tell staff from anonymous visitors, so you fall back to IP address exemptions instead. The two mechanisms are alternatives, not a menu you can freely combine.
The deployment reason in the note is the sharpest one. During a migration the database is exactly what is not working, so a maintenance page that needs a session lookup against that database is a maintenance page that fails.
The state backends, and what each one costs you
The state is simply whether maintenance mode is on, and where that flag lives is the design decision of the package.
The MAINTENANCE_MODE setting is the on switch, and it defaults to None:
MAINTENANCE_MODE = None# by default, to get/set the state value a local file backend is used
# if you want to use the db or cache, you can create a custom backend
# custom backends must extend 'maintenance_mode.backends.AbstractStateBackend' class
# and implement get_value(self) and set_value(self, val) methods
# (use the 'from_state_to_str_value' / 'from_str_to_state_value' class methods
# to serialize/deserialize the state value, they support both plain bool values
# and scheduled maintenance state)
MAINTENANCE_MODE_STATE_BACKEND = "maintenance_mode.backends.LocalFileBackend"The state backend decides how the flag is stored, and there are four documented choices.
The LocalFileBackend is the default, and the comment above it explains the reasoning: by default, to get and set the state value a local file backend is used. If you want to use the database or the cache, you can create a custom backend, and custom backends must extend the maintenance_mode.backends.AbstractStateBackend class and implement get_value(self) and set_value(self, val) methods, using the from_state_to_str_value and from_str_to_state_value class methods to serialise and deserialise the state value, and those support both plain bool values and scheduled maintenance state.
That last clause is worth noting, because it goes beyond the obvious. The state is not only a boolean, it can express a schedule, which means the flag can turn maintenance mode on and off at a time rather than only being flipped by hand.
Then there are three named alternatives in the settings file. The DefaultStorageBackend is the default Django storage. The StaticStorageBackend, with the comment that you must make sure STATIC_ROOT and STATIC_URL are also set. And the CacheBackend, with the comment that you can use a custom cache backend by adding a maintenance_mode entry to settings.CACHES, otherwise the default cache backend will be used.
The cache backend has two extra settings. The MAINTENANCE_MODE_CACHE_BACKEND names the cache from settings.CACHES to use, defaulting to None, which means a cache named maintenance_mode will be used if present in settings.CACHES and otherwise the default cache is used. And MAINTENANCE_MODE_STATE_BACKEND_FALLBACK_VALUE is the value backends return in case of failure, which the comment says is actually only used by the cache backend:
MAINTENANCE_MODE_STATE_BACKEND_FALLBACK_VALUE = FalseThat fallback is a small piece of good design. A cache is the one backend that can fail transiently, and a cache failure should not mean the site silently stops serving, so the default is False, meaning off, which fails open rather than closed.
The file backend has its own path setting, and the reason it exists is stated:
MAINTENANCE_MODE_STATE_FILE_PATH = "maintenance_mode_state.txt"By default a file named maintenance_mode_state.txt is created in the settings.py directory, and you can customise the state file path in case the default one is not writable. That is a real deployment concern, because a settings directory is often read-only in a container image, and a maintenance page that cannot write its own flag is a maintenance page that cannot be turned off.
The exemptions, which are the bulk of the settings
The remaining settings are all about who gets through while the site is closed, and there are more of them than you would expect.
There is MAINTENANCE_MODE_IGNORE_ADMIN_SITE, where if True the admin site will not be affected by the maintenance mode page. Then there is a set of user-based exemptions, each its own setting. MAINTENANCE_MODE_IGNORE_ANONYMOUS_USER means anonymous users will not see the maintenance page. MAINTENANCE_MODE_IGNORE_AUTHENTICATED_USER means authenticated users will not see it. MAINTENANCE_MODE_IGNORE_STAFF excludes staff. And MAINTENANCE_MODE_IGNORE_SUPERUSER excludes the superuser.
Separating anonymous, authenticated, staff and superuser is deliberate. A site that closes for maintenance still needs its administrators to get in, and one boolean for staff would not distinguish an ordinary signed-in user from somebody who can actually fix the problem. Four separate flags let you say that staff can get in but regular members cannot.
Then the network-level exemptions. MAINTENANCE_MODE_IGNORE_IP_ADDRESSES is a list of IP addresses that will not be affected, and the comment explains that the addresses are compiled into regular expression objects, which is how you match a range or a prefix rather than one exact address.
That mechanism needs a client IP address, and getting the right one is a classic source of bugs behind a reverse proxy, where the address you see is the proxy's. The setting for it is MAINTENANCE_MODE_GET_CLIENT_IP_ADDRESS, which is the path of the function that will return the client IP address given the request object, with the default function, maintenance_mode.utils.get_client_ip_address, returning request.META['REMOTE_ADDR']. The comment adds that in some cases the default function returns None, and to avoid that scenario just use django-ipware:
MAINTENANCE_MODE_GET_CLIENT_IP_ADDRESS = "ipware.ip.get_ip"So the recommended setup for a site behind a proxy is to install django-ipware and point the setting at ipware.ip.get_ip, and the setting is a dotted path string rather than a callable, which lets you change it without a code change in the package.
One more path setting exists for the response context, the path of the function that will return the response context, following the same myapp.mymodule.myfunction convention. So the package is built around the idea that you supply callables by dotted path for the things that differ per deployment: the client IP address and the template context. That is a design that keeps the package free of configuration for those cases and puts the choice in your project.
The project, and how to evaluate it
The repository is a conventional, well-tooled Python package. There is a maintenance_mode/ package directory, tests/, a pyproject.toml and a setup.py, requirements.txt and requirements-test.txt, a tox.ini, a runtests.py, a MANIFEST.in, a CHANGELOG.md, SECURITY.md, a CODE_OF_CONDUCT.md and a .pre-commit-config.yaml.
The badge set is a good summary of the project's quality posture: Python and Django version ranges, PyPI download counts, a pre-commit.ci badge, a GitHub Actions test badge, Codecov coverage, a Codacy grade, black for code style, ruff for linting and an OpenSSF Scorecard badge. A package that publishes a supply chain scorecard badge is a package whose author is thinking about provenance, and that matters more than usual for something you install to serve an error page during an incident.
The classification in the manifest says Development Status 5, Production/Stable, and the topic is Build Tools rather than something more specific.
For an evaluation, four things decide it.
First, and most importantly, whether your failure mode is the one this package handles. If your site is up and you want to close it, this is exactly the right tool. If the site is down, or the database is down, or the code will not import, then this package cannot help, because the middleware never runs, and you need something at the web server or load balancer level instead. The README says this plainly, and the note about database migrations and automated deployments is the case where the distinction bites.
Second, whether the exemptions match your deployment. If you run behind a proxy and want to exempt specific addresses, you need the client IP setting to be right, and the default reads REMOTE_ADDR, which behind a proxy is the proxy. Install django-ipware and point the setting at it, as the documentation suggests.
Third, where the flag lives. A local file is the default and is the most dependable choice during a database outage, but it needs a writable path, and the settings file path exists because the default location is the settings directory, which is often read-only in a container.
Fourth, the ordering trade. If you want the page to survive a database outage, move the middleware up and use the IP and URL exemptions, accepting that the user-based ones stop working. If you want staff to get in by role, leave it last and accept that it needs the session and auth middleware to run, which needs the database.
What you get is a small, well-documented package that closes a site in a controlled way without taking the database down with it, with enough exemption options to keep the people who need access able to get in. The cost is that it is an application-level switch, and it is not a substitute for a server-level maintenance page.
Editorial conclusion
Adopt django-maintenance-mode if your Django site needs to be closed to ordinary users while it stays running, for a migration, a data import or a deliberate freeze, since the state can live in a file and does not need the database. Do not adopt it as your disaster recovery plan, because it runs inside the application, so when the process is down or the code will not boot the page never appears and you need a server-level maintenance response instead. Before you commit, decide where the flag lives and make sure the state file path is writable, since the default sits in the settings directory and is often read-only in a container, and check the middleware ordering against your exemptions, because moving it higher up to survive a database outage costs you the user-based exemptions.
Frequently asked questions
What does django-maintenance-mode do?
It shows a 503 error page when maintenance mode is on. It works at application level, so your Django instance has to be up, and it does not use the database and does not prevent database access, which is what makes it usable during a migration.
How do I install django-maintenance-mode?
Install it with pip, add maintenance_mode to settings.INSTALLED_APPS before your custom applications, add maintenance_mode.middleware.MaintenanceModeMiddleware to settings.MIDDLEWARE as the last middleware, add your own templates/503.html file, and restart the application server. The supported Django versions are 4.2 through 6.1 on Python 3.11 through 3.14.
Where does django-maintenance-mode store the on and off state?
The state backend setting decides. The default is a local file backend, and the alternatives are the default Django storage, a static storage backend which needs STATIC_ROOT and STATIC_URL set, and a cache backend which can use a cache entry named maintenance_mode from settings.CACHES. Custom backends extend AbstractStateBackend and implement get_value and set_value.
Who can still reach the site while maintenance mode is on?
There are separate settings for the admin site, anonymous users, authenticated users, staff and superusers, plus a list of IP addresses compiled into regular expression objects. The IP exempt list needs the correct client address, so behind a proxy you should point MAINTENANCE_MODE_GET_CLIENT_IP_ADDRESS at django-ipware rather than relying on the default that reads request.META.
Why should the middleware position be changed in some cases?
By default it runs last, so session and auth middleware still execute during maintenance mode, which is what makes the user-based exemptions work. Moving it higher up skips those, which is what you want during database migrations or automated deployments, but it also disables features such as request.user, so the user-based exemptions stop working and you fall back to the URL and IP address ones.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fabiocaccamo-django-maintenance-mode)Was this useful?