skip to content

Which email backends does Django ship, and which would you configure for local development, automated tests and production?

level: middleimportance: should knowfreq 48%

answer

  1. five classes under core.mail.backends
  2. stdout, files, memory, nothing
  3. test runner swaps it for you
  4. check --deploy catches the wrong one

basics

~20 s

Django ships smtp, console, filebased, locmem and dummy backends. Use console or filebased in development, let the test runner swap in locmem for tests, and use SMTP or a provider's backend in production. Only SMTP is meant for production.

solid answer

~30 s

The backend is the class that actually delivers an `EmailMessage`, and all five live under `django.core.mail.backends`. `smtp.EmailBackend` talks to a real SMTP server and is the only built-in intended for production. `console.EmailBackend` writes each message to stdout, and since Django 6.1 the `startproject` settings template makes it the default mailer. `filebased.EmailBackend` writes messages to files in a required `file_path` directory. `locmem.EmailBackend` appends them to `django.core.mail.outbox`, and Django's test runner switches every configured mailer to it automatically. `dummy.EmailBackend` discards everything. In 6.1, `manage.py check --deploy` reports `mail.E001` if the `"default"` mailer still points at one of the four development backends.

code

python · 21 lines
python
# settings/dev.py
MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.filebased.EmailBackend",
        "OPTIONS": {"file_path": BASE_DIR / "tmp" / "sent-mail"},
    },
}

# settings/prod.py
MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "smtp.example.net",
            "use_tls": True,  # port defaults to 587
            "username": os.environ["SMTP_USER"],
            "password": os.environ["SMTP_PASSWORD"],
            "timeout": 10,
        },
    },
}

go deeper

for a junior

Name the five built-in backends and say which one prints to the console and which one tests use.

for a middle

Explain how the test runner overrides every mailer with locmem, what file_path and stream do, and how SMTP picks a port.

for a senior

Show you guard production with check --deploy and mail.E001, and verify real delivery with sendtestemail after deploys.

for a principal

Decide whether developers see real rendered mail locally (file backend or a local SMTP sink) and make that the team's documented default.

## What an email backend is Django separates **building** a message (`EmailMessage`) from **delivering** it. Delivery is done by an **email backend**, a class with `open()`, `close()` and `send_messages(email_messages)`. It returns how many messages it delivered and can be used as a context manager. `send_mail()`, `EmailMessage.send()` and `mail_admins()` all end up calling a backend's `send_messages()`. Swapping the backend therefore changes where every email goes without touching any sending code. ## The five built-in backends | Backend path (`django.core.mail.backends.…`) | What it does | Options | Intended for | |---|---|---|---| | `smtp.EmailBackend` | Connects to an SMTP server and sends | `host` (required), `port`, `username`, `password`, `use_tls`/`use_ssl`, `timeout`, `ssl_certfile`/`ssl_keyfile` | production | | `console.EmailBackend` | Writes the whole message to a stream | `stream` (defaults to stdout) | development | | `filebased.EmailBackend` | Writes messages to a new file per connection | `file_path` (required, created if missing) | development | | `locmem.EmailBackend` | Appends messages to `django.core.mail.outbox` | none | tests, development | | `dummy.EmailBackend` | Does nothing | none | development | With the SMTP backend, an omitted `port` follows the security option: `587` with `use_tls`, `465` with `use_ssl`, otherwise `25`. Setting both `use_tls` and `use_ssl` is rejected as an invalid mailer. ## Local development - **Console** is the easiest choice: every email appears in the `runserver` output. A new Django 6.1 project already has `MAILERS = {"default": {"BACKEND": "django.core.mail.backends.console.EmailBackend"}}` in `settings.py`. - **File-based** is better when you want to open messages later or diff them. Each connection writes its own file into `file_path`. - For a more realistic check, Django's docs suggest running a throwaway local SMTP server (the `aiosmtpd` package) and pointing the SMTP backend's `host`/`port` at it. - **Dummy** is for when you want sending code paths to run with no output at all. ## Automated tests Django's test runner overrides the mail configuration for the whole run. If `MAILERS` is defined, **every alias** is rewritten to the locmem backend. Otherwise `EMAIL_BACKEND` is set to it. `mail.outbox` is reset to an empty list. So tests never send real email, even when settings point at a production relay. In 6.1, each message in the outbox also carries `sent_using`, the alias that sent it. How to assert on the outbox and isolate it between tests belongs to the testing leaf. The point here is that you do **not** need a test-only backend setting. ## Production, and the deploy check In production you use `smtp.EmailBackend` pointed at your relay or provider, or a third-party backend that calls a provider's HTTP API. Django 6.1 adds two system checks: 1. **`mail.W001`** (a warning): `MAILERS` is defined but has no `"default"` entry, so sending without `using=` will fail. 2. **`mail.E001`** (an error, only under `manage.py check --deploy`): the `"default"` mailer uses the console, dummy, file-based or locmem backend, so "email will not be sent". That second check catches the classic "password-reset emails vanished after deploy" bug. The development settings leaked into production and messages went to a container's stdout. ## Custom and third-party backends When the built-ins don't fit, you can write a backend or install one: - A custom backend subclasses `django.core.mail.backends.base.BaseEmailBackend` and implements `send_messages(email_messages)`, returning the number delivered. It adds `open()` and `close()` if it keeps a connection. - Its constructor receives the alias's `OPTIONS` as keyword arguments. Since 6.1 the base class warns about options it does not recognise, and Django 7.0 will reject them. A backend must consume its own options before calling `super().__init__()`. - Community packages provide backends that call a provider's HTTP API instead of SMTP, hand messages to a task queue, or wrap another backend to enforce do-not-send lists or log what was sent. Because every sending API goes through the configured backend, you can swap SMTP for a provider API with a settings change and no code change. ## Configuring per environment With `MAILERS` (Django 6.1) the backend is `MAILERS[alias]["BACKEND"]`, and an entry without `BACKEND` defaults to SMTP. On projects still using the deprecated `EMAIL_BACKEND` and `EMAIL_*` settings, the global default is the SMTP backend on `localhost:25`. That default goes away in 7.0, when sending without `MAILERS` raises `MailerDoesNotExist`. You cannot mix the two styles: defining `MAILERS` alongside any deprecated `EMAIL_*` setting raises `ImproperlyConfigured`. To smoke-test the real configuration after a deploy, run `manage.py sendtestemail [email protected]`, adding `--using <alias>` in 6.1 to target a specific mailer.

  • Why might you pick the file-based backend over the console backend in development?
    The console backend prints to stdout, where messages scroll away and interleave with request logs. The file-based backend writes each connection's messages into the `file_path` directory, so you can open them later, keep them across restarts, or diff the rendered output after changing a template. Both are flagged by `mail.E001` if they end up as the production default.
  • How do you check after a deploy that production can actually send mail?
    Run `manage.py check --deploy` so `mail.E001` catches a development backend, then `manage.py sendtestemail [email protected]`. It sends a real message through the configured default mailer, or through a specific alias with `--using` in 6.1. `--admins` and `--managers` target those settings' addresses instead.

saying these in an interview costs you the question

  • The console backend is acceptable in production because logs capture stdout.
  • Tests send real email unless you mock send_mail() in every test.
  • The test runner swaps only the default mailer, not the other aliases.
  • The file-based backend needs no directory configured.
  • The dummy backend records messages so tests can inspect them.