Setting Django's DEBUG safely

Django’s DEBUG setting is a powerful tool. Turning it on fundamentally changes Django’s behavior under the hood, making it easier to debug an application and simpler to run it in development.

However, to enable those benefits, the behavioral changes also expose critical performance and security issues such as

  • exposing stack traces and configuration details to attackers,
  • disabling error reporting,
  • degrading database query performance,
  • serving non-minified static files.

While not a concern in a developer’s local environment, these issues can be catastrophic if applied to a production app.

A common beginner mistake when deploying a Django app is to do so with DEBUG set to True. Even for experts, it’s an easy one to have slip through the cracks. Let’s expand on why this is a problem and how you can resolve it.

Why DEBUG=True is bad

DEBUG=True does not just enable pretty error pages. It changes the behavior of Django in a number of places. Here is a list of files that reference it:

$ grep -rl --include="*.py" "settings.DEBUG" . | grep -v test
./django/middleware/common.py
./django/core/checks/security/base.py
./django/core/management/commands/runserver.py
./django/core/handlers/exception.py
./django/core/handlers/asgi.py
./django/core/handlers/base.py
./django/dispatch/dispatcher.py
./django/template/backends/django.py
./django/template/backends/jinja2.py
./django/template/context_processors.py
./django/template/defaulttags.py
./django/utils/log.py
./django/contrib/messages/middleware.py
./django/contrib/auth/admin.py
./django/contrib/admin/options.py
./django/contrib/admin/widgets.py
./django/contrib/staticfiles/management/commands/runserver.py
./django/contrib/staticfiles/utils.py
./django/contrib/staticfiles/storage.py
./django/contrib/staticfiles/urls.py
./django/contrib/staticfiles/views.py
./django/contrib/flatpages/middleware.py
./django/http/request.py
./django/db/backends/base/base.py
./django/views/csrf.py
./django/views/debug.py
./django/conf/urls/static.py

The short of it is: your app will not behave the same when DEBUG=True. When you start depending on DEBUG=True behavior, you’re going to have issues when you set DEBUG=False.

These behavioral changes range from serious performance issues and major security risks to unexpected configuration and logging behavior.

Critical risks

Let’s dig into the areas that are particularly problematic first.

DEBUG=True exposes debug information about your site to everyone on the internet:

  • Django’s debug view is shown for response errors: 400s; 404s; 500s (unless DEBUG_PROPAGATE_EXCEPTIONS=True). This view displays a detailed traceback, metadata about your environment, and all currently defined Django settings. The debug view is also shown for suspicious operations.
  • The stacktrace is returned in a plain text response for uncaught exceptions at the server level.
  • Template debug mode is enabled: views show exception details and tracebacks for errors encountered during template rendering.
  • Any rendered debug template tag outputs a ton of debugging info.

The debug page attempts to redact settings that might be sensitive, but this is done merely by matching common security-related keywords to setting names. It should not be considered effective protection for your settings.

There is plenty of other information shown on the debug page which could be useful to attackers:

  • filesystem paths
  • installed packages
  • middleware used
  • cache and database backends
  • all URL patterns
  • deployment configuration

Operational risks

Less minification and more logging negatively affects performance in many parts of your app’s stack:

  • Django’s “grossly inefficient” (and “probably insecure”) static files route is exposed to serve static files directly from the server, rather than from a dedicated static file service. This occurs when using runserver or the static() URL patterns.
  • The admin site uses non-minified versions of jQuery, Select2, and XRegExp static files.
  • Database query logging is enabled, increasing memory usage and degrading database query and transaction performance.
  • There is additional logging of methods’ “mode” (i.e. synchronous or asynchronous) being adapted with async_to_sync() or sync_to_async(), adding overhead.
  • When using Jinja2, its auto reload is turned on. The loader checks on disk if the source has changed each time a template is requested.
  • When using the django.template.loaders.cached.Loader template loader wrapper, a unique TemplateDoesNotExist object is cached for each missing template to preserve debug data, increasing memory usage.
  • ManifestStaticFilesStorage uses non-hashed file names, resulting in stale cached static files for users.

In general, sites with DEBUG=True will use more memory, spend more CPU cycles on logging, and less efficiently serve responses. They will also send less notifications about errors:

  • BrokenLinkEmailsMiddleware will not send broken link emails on 404s.
  • With the default logging configuration, all log records are sent to the console and exceptions are not emailed to the ADMINS email addresses.

Behavioral differences

Enabling DEBUG results in more surprising user-visible errors:

  • Making a write request (DELETE, POST, PUT, PATCH) to any URL without a trailing slash raises a RuntimeError. This occurs if APPEND_SLASH=True.
  • With the messages framework, an exception is raised if there is not enough space to store all messages across a request. This can occur when using CookieStorage, which has a max storage of 2048 bytes.
  • If FlatpageFallbackMiddleware is installed, non-404 exceptions raised during flatpage retrieval are re-raised, rather than being swallowed and returning the original 404 response.
  • Errors are raised if any signal is connected to a “bad” receiver (e.g. not callable or doesn’t accept kwargs).
  • Instead of a 403, a 404 is raised for admin users attempting to create a new user in the default UserAdmin if they have the add permission but lack the change permission.

Server startup can result in unexpected behavior:

  • If ALLOWED_HOSTS is falsy, it defaults to [".localhost", "127.0.0.1", "[::1]"]. In this case, your app will not be accessible from your production domain.
  • Django raises an error at startup if MEDIA_URL is within STATIC_URL.
  • Deployment checks (i.e. checks --deploy) warn about DEBUG=True.

And additional logging:

  • Django logs warnings about installed middleware that is disabled.
  • Django warns on every csrf_token tag rendered without using RequestContext.

When to use DEBUG=True

Only set DEBUG=True in local development environments.

A low-level deployed environment that serves as a sort of developer sandbox doesn’t count. Once it’s on the internet, DEBUG=False for all the reasons noted above (especially critical security issues). The same applies to preview, QA, and staging deployments. If a system is reachable from the public internet, assume attackers can reach it too.

This is a great opportunity for developers to learn how to debug live systems. You’ll get better at logging and probably want to have an error reporting system like Sentry.

Django defaults to DEBUG=True to optimize for the developer onboarding experience: a new project works in a local development environment immediately after startproject. Their documentation warns several times against enabling DEBUG in production.

How to avoid it

The obvious answer here is to set DEBUG=False in your settings. But if that’s something you have to remember every time you deploy your app to a new environment, it’s going to get forgotten at some point.

Failing closed

With firewalls, we talk about “failing closed”. The idea is that the default is locked down and you have to take action to open things up. When I setup Django projects, I take this approach with any security related settings. The default is production-ready and secure, but you can override those settings for local development.

So make your settings default to DEBUG=False and allow it to be toggled using your preferred mechanism .env file, DJANGO_SETTINGS_MODULE override, etc.

Caution when setting via the environment

Even if you default to False, you can accidentally set it to True if you aren’t handling environment variables properly. Take this example:

import os

# ❌ DON'T DO THIS
DEBUG = os.environ.get("DEBUG", "False") != "False"

What’s wrong here? Consider if I set the environment to DEBUG=false or DEBUG=0. Both of those scenarios that might look like they should be false, will evaluate to True.

I recommend using something like django-environ or goodconf to cast environment variables to their expected types (and raise an error if the value is unexpected). If you don’t want the extra dependency, something like this is a bit verbose, but much more reliable:

import os

def _parse_bool_env(env_var: str, default: bool) -> bool:
    if env_var not in os.environ:
        return default
    value = os.environ.get(env_var)
    if value.lower() in ("true", "1", "yes", "on"):
        return True
    elif value.lower() in ("false", "0", "no", "off", ""):
        return False
    err_msg = f"Invalid boolean value for {env_var}: {value}"
    raise ValueError(err_msg)

DEBUG = _parse_bool_env("DEBUG", False)
James Osgood

About the author

James Osgood

Starting with his first job writing Selenium tests and creating Django-backed web apps, James has been living and breathing Django and TDD throughout his career. In team lead positions, he learned the importance of recruiting the …

View James's profile