System Checks
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06) on Python 3.12 to 3.14, with the 5.2 LTS differences noted where they matter, and was checked on 2026-10-03. The examples ran on Django 6.1.1 and Python 3.13 with SQLite, in a throw-away project with a small catalog app (the Bookshelf catalogue that grows across this section). Nothing here needs PostgreSQL or Redis; the checks that talk to a database ran against SQLite.
Django validates a project before it does most things with it. The system check framework is a set of static checks that detect common problems, such as a field without max_length, a URL pattern that can never match, or DEBUG = True in production, and that come with hints for fixing them. The framework is open: your own apps, fields, models and reusable packages can add checks, and check --deploy turns the same machinery into a pre-flight audit of your production settings. This page covers when checks run, the check command, the message and tag model, writing and testing custom checks, silencing, the deployment checks and how to use them in CI.
What runs, and when
Checks are triggered explicitly by the check command and implicitly before most other management commands, including runserver and migrate. Serious problems (level ERROR or higher) stop the command from running; minor ones are printed and the command carries on. Two facts are worth remembering.
-
Checks do not run in the WSGI or ASGI stack that serves requests in deployment, for performance reasons. A gunicorn or Uvicorn worker starts happily with a configuration that
checkwould reject. If you want checks on the deployment server, runcheckexplicitly, for example as a CI step or an entrypoint step (see Using the checks in CI and deployment). -
A management command can opt out.
--skip-checksskips the checks that run before the command (it exists when the command’srequires_system_checksis not empty), and a command can narrow the checks it needs by listing tags (see Checks and your own management commands).
The framework sits between the application registry and the command: Django loads settings and apps (calling each AppConfig.ready()), the checks registered by then are collected, and the command runs only if none of them reports a serious issue.
Source: System check framework.
The check command
check runs the registered checks and prints what it found, grouped by severity. With no issues the output is a single line, and the exit status is zero.
python manage.py check
python manage.py check --list-tags
python manage.py check catalog auth
python manage.py check --tag models --tag compatibility
python manage.py check --database default
python manage.py check --deploy --settings=config.production
python manage.py check --fail-level WARNING
System check identified no issues (0 silenced).
Source: django-admin check.
| Option | Effect |
|---|---|
|
Restricts the checks to the given apps (the check function receives them as |
|
Runs only the checks registered with the tag; repeat the option for several tags. An unknown tag is an error: |
|
Prints the available tags. Add |
|
Runs the checks that need database access against the given alias; repeat for several aliases. Without it, checks tagged |
|
Adds the checks that only make sense for a production settings file. |
|
The lowest level that makes the command exit with a non-zero status: |
The default exit status is non-zero only for ERROR and CRITICAL, which is why a CI job that must also fail on warnings needs --fail-level WARNING (see Using the checks in CI and deployment).
Messages, levels and identifiers
Every finding is a CheckMessage with a level, a short message (under 80 characters, no newlines), an optional one-line hint, an optional object that gives context, and an identifier. The levels are DEBUG (10), INFO (20), WARNING (30), ERROR (40) and CRITICAL (50), and the shortcut classes Debug, Info, Warning, Error and Critical in django.core.checks set the level for you. Identifiers follow the pattern applabel.X001, where the letter is one of C, E, W, I or D for the severity: models.E…, security.W004, catalog.E001. The identifier is what you use to silence a check and to assert on it in tests, so keep it stable and unique within your app.
Messages with a level of ERROR or higher make Django refuse to run management commands; lower ones are reported and can be silenced.
Built-in tags
Checks are labelled with tags so that a subset can be run. The built-in tags are available as django.core.checks.Tags.
| Tag | What it covers |
|---|---|
|
Admin site declarations. |
|
Asynchronous-related configuration, such as |
|
The |
|
Potential problems after a Django upgrade. |
|
Configuration of custom management commands. |
|
Database-related configuration. These checks do more than static analysis, so they are not run by default: only by |
|
File-related configuration. |
|
Email configuration. |
|
Model, field and manager definitions. |
|
Security-related configuration (most deployment checks). |
|
Signal declarations and handler registrations. |
|
|
|
|
|
Template-related configuration. |
|
Translation configuration. |
|
URL configuration. |
A check may carry several tags, and any string is a valid tag, so your own checks can use an app-specific tag such as catalog. check --list-tags prints the tags actually registered in your project, including custom ones.
Source: Built-in tags.
Writing a custom check
A check is a function that accepts app_configs and **kwargs and returns a list of messages, empty when everything is fine. app_configs is the list of apps to inspect, or None meaning all installed apps. **kwargs is required for future expansion; it currently carries databases. Register the function with the register decorator, optionally with tags.
Checks must be registered in a module that is loaded when the application loads. The convention is to import the module from AppConfig.ready().
from django.apps import AppConfig
class CatalogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "catalog"
def ready(self):
from . import checks # noqa: F401 (registers the checks)
from django.conf import settings
from django.core.checks import Error, Tags, Warning, register
@register("catalog")
def check_page_size(app_configs, **kwargs):
errors = []
size = getattr(settings, "BOOKSHELF_PAGE_SIZE", None)
if not isinstance(size, int) or isinstance(size, bool) or size < 1:
errors.append(
Error(
"BOOKSHELF_PAGE_SIZE must be a positive integer.",
hint="Set BOOKSHELF_PAGE_SIZE to a value such as 20.",
obj=settings,
id="catalog.E001",
)
)
return errors
@register("catalog", Tags.security, deploy=True)
def check_cdn_url(app_configs, **kwargs):
errors = []
if not getattr(settings, "BOOKSHELF_CDN_URL", ""):
errors.append(
Warning(
"BOOKSHELF_CDN_URL is empty, so cover images are served by Django.",
hint="Point BOOKSHELF_CDN_URL at the CDN that fronts MEDIA_URL.",
id="catalog.W001",
)
)
return errors
Source: Writing your own checks.
The first check always runs. With BOOKSHELF_PAGE_SIZE = 0 in the settings it stops every command, and the output points at the object and carries the hint.
SystemCheckError: System check identified some issues:
ERRORS:
<Settings "config.settings">: (catalog.E001) BOOKSHELF_PAGE_SIZE must be a positive integer.
HINT: Set BOOKSHELF_PAGE_SIZE to a value such as 20.
System check identified 1 issue (0 silenced).
The second one is registered with deploy=True, so it is invisible to a plain check and runs only with --deploy:
$ python manage.py check --deploy --tag catalog
System check identified some issues:
WARNINGS:
?: (catalog.W001) BOOKSHELF_CDN_URL is empty, so cover images are served by Django.
HINT: Point BOOKSHELF_CDN_URL at the CDN that fronts MEDIA_URL.
System check identified 1 issue (0 silenced).
register can also be called as a plain function, register(my_check, Tags.compatibility), which is equivalent to decorating it; that is handy for registering a callable you do not own. Tags can be Tags members or any string.
Source: Registering and labeling checks.
Checks that use the database
A check function receives a databases keyword argument: the list of aliases whose connections it may use to inspect database-level configuration. Register such checks with Tags.database so they only run when someone asks for database checks (migrate, --database, or --tag database).
@register(Tags.database)
def check_database_vendor(app_configs, databases=None, **kwargs):
if databases is None:
return []
from django.db import connections
return [
Warning(
f"Database {alias!r} uses SQLite, which is not meant for production.",
id="catalog.W002",
)
for alias in databases
if connections[alias].vendor == "sqlite"
]
$ python manage.py check
System check identified no issues (0 silenced).
$ python manage.py check --database default
System check identified some issues:
WARNINGS:
?: (catalog.W002) Database 'default' uses SQLite, which is not meant for production.
System check identified 1 issue (0 silenced).
Source: Writing your own checks.
Django 6.1 changed what a check that is not tagged database receives. The release notes ("System checks", backwards-incompatible changes) say the check command now supplies all databases when none is specified; in the framework code, when no alias is given, databases is the list of every configured alias instead of None. Earlier versions documented databases=None as "do not use database connections". A check that reads databases should therefore still tag itself database if it opens connections, and callers of run_checks() should expect connections to be touched. Nothing changes for checks that ignore databases.
Piggybacking on fields, models, managers and more
Fields, constraints, models, model managers, template engines, task backends and database backends already implement a check() method that is registered with the framework, so you do not call register for them. Extend the method, call super(), append your own messages and return the list; put each check in a separate helper. A custom field is the classic case.
from django.core import checks
from django.db import models
class RatingField(models.PositiveSmallIntegerField):
"""An integer between ``min_value`` and ``max_value`` (inclusive)."""
def __init__(self, min_value=1, max_value=5, **kwargs):
self.min_value = min_value
self.max_value = max_value
super().__init__(**kwargs)
def check(self, **kwargs):
errors = super().check(**kwargs)
errors.extend(self._check_min_max_values())
return errors
def _check_min_max_values(self):
if self.min_value > self.max_value:
return [
checks.Error(
"min_value must not be greater than max_value.",
hint="Decrease min_value or increase max_value.",
obj=self,
id="catalog.E002",
)
]
return []
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs["min_value"] = self.min_value
kwargs["max_value"] = self.max_value
return name, path, args, kwargs
Using RatingField(min_value=5, max_value=1) in a Review model makes every command fail until it is fixed:
SystemCheckError: System check identified some issues:
ERRORS:
catalog.Review.rating: (catalog.E002) min_value must not be greater than max_value.
HINT: Decrease min_value or increase max_value.
System check identified 1 issue (0 silenced).
On a model the method is a class method, not an instance method. The same approach works on a Manager subclass.
from django.core import checks
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
@classmethod
def check(cls, **kwargs):
errors = super().check(**kwargs)
if not any(f.name == "slug" and f.unique for f in cls._meta.get_fields()):
errors.append(
checks.Error("Book needs a unique slug.", obj=cls, id="catalog.E003")
)
return errors
Template engines gained a check() method in Django 5.1, and constraints in 6.0 (in older versions constraints did not implement check()); the task backends of the Tasks framework, new in 6.0, have one too. The overridable-check() approach therefore covers more objects than most tutorials show.
Testing checks
Messages are comparable, so a unit test can build the expected message and compare lists. Use SimpleTestCase when no database is needed, override_settings to vary the setting, and call_command("check") for an integration test that proves the check is registered and reaches the command. The tests below ran with python manage.py test catalog.
from io import StringIO
from django.core import checks
from django.core.management import call_command
from django.core.management.base import SystemCheckError
from django.test import SimpleTestCase, override_settings
from .checks import check_cdn_url, check_page_size
from .fields import RatingField
from .models import Book
class PageSizeCheckTests(SimpleTestCase):
@override_settings(BOOKSHELF_PAGE_SIZE=0)
def test_rejects_non_positive_page_size(self):
errors = check_page_size(None)
self.assertEqual([e.id for e in errors], ["catalog.E001"])
@override_settings(BOOKSHELF_PAGE_SIZE=20)
def test_accepts_positive_page_size(self):
self.assertEqual(check_page_size(None), [])
@override_settings(BOOKSHELF_PAGE_SIZE=0)
def test_integration_through_check_command(self):
message = "(catalog.E001) BOOKSHELF_PAGE_SIZE must be a positive integer."
with self.assertRaisesMessage(SystemCheckError, message):
call_command("check", "--tag", "catalog")
class CdnDeployCheckTests(SimpleTestCase):
@override_settings(BOOKSHELF_CDN_URL="")
def test_warns_when_cdn_url_is_empty(self):
stderr = StringIO()
call_command("check", "--tag", "catalog", "--deploy", stderr=stderr)
self.assertIn("(catalog.W001)", stderr.getvalue())
@override_settings(BOOKSHELF_CDN_URL="https://cdn.example.com/")
def test_silent_when_cdn_url_is_set(self):
self.assertEqual(check_cdn_url(None), [])
class RatingFieldCheckTests(SimpleTestCase):
def test_min_greater_than_max_is_an_error(self):
field = RatingField(min_value=5, max_value=1)
self.assertEqual(
field.check(),
[
checks.Error(
"min_value must not be greater than max_value.",
hint="Decrease min_value or increase max_value.",
obj=field,
id="catalog.E002",
)
],
)
def test_default_bounds_are_valid(self):
self.assertEqual(RatingField().check(), [])
def test_book_model_has_no_check_messages(self):
self.assertEqual(Book.check(), [])
Ran 8 tests in 0.002s
OK
Two details matter. A warning does not raise SystemCheckError, so a warning is asserted by capturing stderr and searching it, as the deploy test does; an error is asserted with assertRaisesMessage. And --tag catalog keeps the integration test focused on your own check instead of every check in the project.
Source: Writing tests and Writing integration tests.
Checks and your own management commands
A custom command controls which checks run before it. requires_system_checks is a list or tuple of tags (for example [Tags.models, "catalog"]) or '__all__', which is the default. In Django 5.2, BaseCommand.get_check_kwargs() was added so a command can change the arguments passed to check(), for instance to opt into database-related checks.
from django.core.checks import Tags
from django.core.management.base import BaseCommand
from catalog.models import Book
class Command(BaseCommand):
help = "Print every book title."
# Run only the model checks and the catalog-tagged checks first.
requires_system_checks = [Tags.models, "catalog"]
def add_arguments(self, parser):
parser.add_argument("--database", default="default")
def get_check_kwargs(self, options):
kwargs = super().get_check_kwargs(options)
return {**kwargs, "databases": [options["database"]]}
def handle(self, *args, **options):
for title in Book.objects.using(options["database"]).values_list("title", flat=True):
self.stdout.write(title)
With a valid configuration the command prints the titles; with BOOKSHELF_PAGE_SIZE = 0 it exits with the catalog.E001 error before handle() runs, and --skip-checks bypasses the checks and runs it anyway. The same error prevents runserver from starting, and runserver --skip-checks starts the server (the request returned 200 in the run). Use --skip-checks for a deliberate, temporary reason, not to make a failing project start.
Source: BaseCommand.requires_system_checks and BaseCommand.get_check_kwargs. See also Management Commands & Shell.
Silencing checks
A warning that you have inspected and accepted can be hidden by listing its identifier in SILENCED_SYSTEM_CHECKS. The count in the summary line records how many messages were hidden, so silenced checks stay visible in aggregate.
# config/settings.py
SILENCED_SYSTEM_CHECKS = ["security.W005", "security.W021"]
The best place for a silence is the settings module of the environment where the decision applies, together with a comment saying why. Silencing security.W008 (“SECURE_SSL_REDIRECT` is not `True”) is reasonable when a load balancer already redirects HTTP to HTTPS; silencing it because the warning is noisy is not. Prefer fixing the setting.
The reference says messages below ERROR are the ones that can be silenced. In the scratch project, listing the identifier of an error (catalog.E002) also hid it: check reported "System check identified no issues (1 silenced)" and the exit status was zero. That is behaviour of the current implementation rather than a documented feature, and hiding an error only hides a bug (the field above was still invalid); treat silencing as a tool for warnings.
Source: SILENCED_SYSTEM_CHECKS and CheckMessage. See also Settings & Configuration.
check --deploy
check --deploy adds the checks registered with deploy=True. Most of them are in the security tag and form an automated, low-hanging-fruit checklist for production settings; Django’s reference is explicit that they do not make a site secure, audit code or detect intrusions. Some may not suit your deployment, which is what SILENCED_SYSTEM_CHECKS is for.
You can run it with your development settings, but those usually lack the production values, so point it at the production settings module with --settings (or DJANGO_SETTINGS_MODULE), or run it on the staging or production host itself with the real environment. Because checks are not part of the request stack, this is the only moment they see the deployed configuration.
Running it with the development settings of a project generated by startproject produces many findings, and in 6.1 one is an error. The generated 6.1 settings file defines MAILERS with the console backend, and the new deployment-only check mail.E001 rejects a development-only backend in the default entry:
$ python manage.py check --deploy
SystemCheckError: System check identified some issues:
ERRORS:
?: (mail.E001) Your MAILERS setting uses a development-only email backend in the 'default' entry (django.core.mail.backends.console.EmailBackend).
HINT: Use a production-ready email backend, such as the SMTP backend, otherwise email will not be sent.
WARNINGS:
?: (catalog.W001) BOOKSHELF_CDN_URL is empty, so cover images are served by Django.
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. …
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. …
?: (security.W012) SESSION_COOKIE_SECURE is not set to True. …
?: (security.W016) You have 'django.middleware.csrf.CsrfViewMiddleware' in your MIDDLEWARE, but you have not set CSRF_COOKIE_SECURE …
?: (security.W018) You should not have DEBUG set to True in deployment.
?: (security.W020) ALLOWED_HOSTS must not be empty in deployment.
System check identified 8 issues (0 silenced).
The remedy is a production settings module that reads secrets and hosts from the environment. Secrets never appear in the file: SECRET_KEY comes from the environment, generated once with get_random_secret_key() or secrets.token_urlsafe() and stored in your secret manager.
import os
from .settings import * # noqa: F403
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",")
MAILERS = {
"default": {
"BACKEND": "django.core.mail.backends.smtp.EmailBackend",
"OPTIONS": {"host": os.environ.get("SMTP_HOST", "localhost")},
}
}
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 3600 # raise gradually once HTTPS works everywhere
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
BOOKSHELF_CDN_URL = os.environ.get("BOOKSHELF_CDN_URL", "")
# Reviewed and accepted: not every subdomain is HTTPS-only, and we do not
# want to join the HSTS preload list.
SILENCED_SYSTEM_CHECKS = ["security.W005", "security.W021"]
$ export DJANGO_SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(64))")
$ export DJANGO_ALLOWED_HOSTS=books.example.com
$ BOOKSHELF_CDN_URL=https://cdn.example.com/ \
python manage.py check --deploy --settings=config.production --fail-level WARNING
System check identified no issues (2 silenced).
With a weak key the matching check warns, which is a reminder that SECRET_KEY quality is one of the things --deploy actually verifies (at least 50 characters, at least 5 unique characters, and not the django-insecure- prefix that startproject generates):
$ DJANGO_SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(8))") \
python manage.py check --deploy --settings=config.production
System check identified some issues:
WARNINGS:
?: (security.W009) Your SECRET_KEY has less than 50 characters, less than 5 unique characters, or it's prefixed with 'django-insecure-' …
System check identified 1 issue (1 silenced).
Source: Deployment checklist: run check --deploy and check --deploy.
The deployment security checks
The identifiers below are the ones the reference lists for check --deploy. Several settings are explained in depth on Security and Content Security Policy.
| Check | Meaning | Usual fix |
|---|---|---|
|
|
Add the middleware; |
|
|
Enable HSTS gradually once all of the site is HTTPS; only include subdomains and preload when every subdomain should be HTTPS-only. |
|
|
Enable it. |
|
|
Enable it, or silence it when a load balancer or reverse proxy redirects to HTTPS. |
|
|
Generate a long random key and read it from the environment. |
|
|
Set |
|
|
Keep the default |
|
|
Set it when the CSRF token lives in a cookie. |
|
|
Set |
|
|
Use |
|
|
List the real host names. |
|
|
Choose a valid policy. |
|
|
Choose a valid value. |
|
A development-only backend in the |
Use the SMTP backend or a provider backend. |
Other security-tagged checks run without --deploy because they validate configuration instead of auditing hardening: security.E101 and E102 for the CSRF failure view, security.E026 (a CSP setting that is not a dictionary) and security.W027 (new in 6.1: the CSP nonce is used without django.template.context_processors.csp). Likewise mail.W001 (new in 6.1) warns about a MAILERS setting without a default entry on every run, while async.E001 (DJANGO_ALLOW_ASYNC_UNSAFE set in deployment) is a deployment-only check in the async_support tag, as the run with the variable set confirmed.
Source: Security checks.
Using the checks in CI and deployment
Checks are cheap, deterministic and need no running server, so they suit a pipeline. The usual arrangement is one job step that fails the build on any finding in the production settings, and one that fails on errors only in the development settings.
# Fail the pipeline on any warning or error in the production configuration.
python manage.py check --deploy --settings=config.production --fail-level WARNING
# Also fail when a model change has no migration.
python manage.py makemigrations --check --dry-run
Run the first step in an environment that has the production environment variables, otherwise os.environ[…] raises KeyError before any check runs. --fail-level WARNING makes the exit status non-zero for warnings as well as errors, so any warning you keep must be silenced explicitly with a comment, which turns "known and accepted" into code review material instead of tribal knowledge. For a container image, the same command is a good last build step or entrypoint pre-check.
Source: check --fail-level.
What changed recently
| Version | Change |
|---|---|
3.2 |
|
4.0 |
|
4.1 |
Support for a boolean |
5.1 |
Template engines implement a registered |
5.2 |
|
6.0 |
Constraints implement a registered |
6.1 |
|
The versions come from the 3.2, 4.0, 4.1, 5.1, 5.2, 6.0 and 6.1 release notes and the 6.1 check reference.
Where older material is outdated
Books and tutorials written for Django 1.11 and 2.x describe check --deploy and SILENCED_SYSTEM_CHECKS correctly in spirit, but the details moved on.
-
The list of deployment warnings is different today.
security.W007(SECURE_BROWSER_XSS_FILTER) was removed in 3.0 because browsers no longer honour theX-XSS-Protectionheader, andsecurity.W017(CSRF_COOKIE_HTTPONLY) was removed in 1.11. Newer ones exist:security.W021(HSTS preload),W022/E023(Referrer-Policy),E024(Cross-Origin-Opener-Policy),W025(SECRET_KEY_FALLBACKS, 4.1), and in 6.x the CSP andMAILERSchecks. -
Compatibility checks have turned into history.
2_0.W001(patterns migrated fromurl()topath()) and4_0.E001(scheme-lessCSRF_TRUSTED_ORIGINS) are the ones that remain in the reference;templates.E001was removed in 5.1 because the conflict now raisesImproperlyConfigured, andmysql.E001becamemysql.W003in 3.1. -
requires_system_checks = TrueorFalsein a custom command no longer works: boolean values were deprecated in 3.2 and removed in 4.1. Use'__all__',[], or a list of tags. -
The tutorial habit of running
check --deployonce by hand is not a process. The warnings that earlier guides tell you to skip (for example HSTS, SSL redirect or secure cookies) are the ones that matter most behind HTTPS, so silence them deliberately with a comment, not by ignoring the output; and remember the checks never run in the serving stack. -
Older guides run
check --deployagainst development settings, whereDEBUGand the generated key make the output noisy. Use a production settings module, as shown above.