One of Django's nice "batteries included" features is the ability to send emails when an error is encountered. This is a great feature for small sites where minor problems would otherwise go unnoticed.

Once your site start getting lots of traffic, however, the feature turns into a liability. An error might fire off thousands of emails in rapid succession. Not only will this put extra load on your web servers, but you could also take down (or get banned from) your email server in the process.

One of the first things you want to do when setting up a high-traffic Django site is replace the default error email functionality with an error reporting service like Sentry. Once you've got Sentry setup, what's the best way to disable error emails? Unfortunately, there isn't one simple answer. Let's dig into why...

How does Django Send the Emails?

First let's look at how Django sends those emails. If you look at the source, you'll find it is defined in the default logging config using standard Python logging:

'django': {
        'handlers': ['console', 'mail_admins'],
        'level': 'INFO',
    }

As you can guess, the mail_admins handler is the one that does the work. It is defined as:

'mail_admins': {
        'level': 'ERROR',
        'filters': ['require_debug_false'],
        'class': 'django.utils.log.AdminEmailHandler'
    }

In plain English, this says that any log messages that are ERROR or higher in the django Python module will get passed to django.utils.log.AdminEmailHandler when the require_debug_false filter evaluates to True.

Here is an example of the default Django logging tree.

Disabling the Logger

Simply removing mail_admins from the handlers should be easy, right? Unfortunately, the answer involves diving into the idiosyncracies of Python logging configuration.

If you search around the internet, you'll come up with a few different options, but rarely an explanation of why they work or the potential drawbacks.

⛔️ Option 1: disable_existing_loggers

One option is to set disable_existing_loggers to True in your LOGGING setting. This parameter is the source of a lot of confusion and the results of using it can be unintuitive. From the Python docs:

The default is True because this enables old behavior in a backward-compatible way. This behaviour is to disable any existing loggers unless they or their ancestors are explicitly named in the logging configuration.

See the effect this has on the logging tree here. Note: all loggers are flagged as "Disabled".

The issue with this approach is that if you later define a logger for a submodule of django, say, django.db. As per the docs, you'll find the default django logger is now re-enabled, mail_admins handler and all.

That's pretty unintuitive behavior and for that reason, I don't recommend this approach. When setting up logging, always set disable_existing_loggers to False to avoid this issue.

⛔️ Option 2: Redefine the Logger

There is a second option which is a little more surgical than option #2. By redefining the logger for django, we will replace the handlers that are defined for it in the default config. Here's a full logging config which will accomplish that:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        # Redefining the logger for the `django` module
        # prevents invoking the `AdminEmailHandler`
        'django': {
            'handlers': ['console'],
            'level': 'INFO',
        },
    }
}

See the effect this has on the logging tree here.

Unfortunately, this change wipes out any handlers set on child loggers. One such potentially helpful handler is the request logging used by Django's runserver command. For that reason, I don't recommend this approach either.

✅ Option 3: Copy the Default Logger

If you'd like to keep Django's default config and just remove the mail_admins handler, your best bet is to simply modify the default dictionary directly.

from copy import deepcopy
from django.utils.log import DEFAULT_LOGGING

logging_dict = deepcopy(DEFAULT_LOGGING)
logging_dict['loggers']['django']['handlers'] = ['console']
LOGGING = logging_dict

See the effect this has on the logging tree here.

This accomplishes the goal, but also introduces some fragility into the code. A change to DEFAULT_LOGGING in a future version of Django could break this code. In practice, you may also find you want to make more changes to the default config (for example, logging to console when DEBUG = False).

✅ Option 4: LOGGING_CONFIG = None

Setting LOGGING_CONFIG = None in your settings prevents Django from setting up any logging at all. This is the nuclear option.

import logging.config
LOGGING_CONFIG = None
logging.config.dictConfig({
    "version": 1,
    "disable_existing_loggers": False,
})

See the effect this has on the logging tree here.

This is a valid approach and probably the best one to take once you want any sort of fine-grained control over your logging. You wouldn't want to use this exact config in practice, but instead, take it as a starting point for building your own custom configuration to suit the needs of your project.


In the process of writing this, I discovered a few "features" in Python logging that surprised me. Brandon Rhode's logging_tree package was invaluable for peeking under the hood after the Django initialized the logging tree. I strongly recommend dropping it into your project to verify logging is setup as you expect.

While this post focuses on the mail_admins functionality, in the real-world, you'll want a more robust logging config (including a catch-all root logger, and a logger for your project module), but we'll leave that for another post.

If you have any Django logging tips, we'd love to hear them!