Sending Email With Django: Configuration, Tests and Durable Delivery
Configure Django email, test messages without external delivery and move production sending into a reliable workflow with explicit failure handling.
TL;DR
- Use Django's console backend to inspect test messages without delivering them.
- Configure the production email backend and credentials separately from local development.
- Send notifications only after the related database transaction commits, and use durable jobs where appropriate.
- Keep submission, delivery and retry states distinct so a successful function call does not become a misleading inbox claim.
Begin with the email backend
Django exposes email helpers through django.core.mail and delegates delivery to a configured backend. This lets application code construct a message while development and production choose different delivery behaviour.
The examples on this page follow Django 6.0's email documentation. Check the documentation for your installed version before copying settings into a newer release. A framework upgrade can change recommended configuration even when the application-level purpose remains the same.
Start with a local backend that does not deliver real messages. That protects test recipients and makes the generated output easy to inspect while you develop the template.
Preview a message in development
In your local settings, configure the console backend:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEFAULT_FROM_EMAIL = "Example App <notifications@example.com>"Then run this original example through the Django shell or a development-only command:
from django.conf import settings
from django.core.mail import send_mail
sent = send_mail(
subject="Your project invitation is ready",
message="Sign in to review your project invitation.",
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=["test-recipient@example.com"],
fail_silently=False,
)
assert sent == 1With the console backend, the message is written to the console rather than delivered. The return value therefore cannot mean that a real mailbox received it. Use placeholder data in this preview and avoid exposing production messages through development logs.
Configure production separately
For Django 6.0 with an SMTP provider that documents STARTTLS on port 587, the relevant settings include the SMTP backend, host, port, username, password, TLS mode and timeout. Load credentials from server-side configuration rather than committing them to the repository.
import os
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = os.environ["SMTP_HOST"]
EMAIL_PORT = 587
EMAIL_HOST_USER = os.environ["SMTP_USER"]
EMAIL_HOST_PASSWORD = os.environ["SMTP_PASSWORD"]
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
EMAIL_TIMEOUT = 15Use your provider's actual settings. Implicit TLS and STARTTLS are different connection modes; do not enable both flags as a troubleshooting shortcut. Confirm that the configured From address is permitted by the provider and authenticated for the sending domain.
Connect email to a committed business event
Suppose a view creates a project invitation inside a database transaction. Sending email before the transaction commits can notify someone about an invitation that disappears if the transaction rolls back.
Django's transaction documentation describes transaction.on_commit() for work that should happen after a successful commit. A common design is to enqueue a notification job after commit. For stronger durability between database state and queue submission, consider a database outbox processed by a worker.
Store the intended notification's identity, recipient and template version. The worker can then record submission attempts and provider results against that identity. Do not create a fresh logical notification each time the same job runs.
Test the message and the lifecycle separately
Use Django's in-memory email backend in application tests to inspect the generated message. Check the intended recipient, subject and content for a representative event. Also verify that the application does not queue a notification when the business transaction fails.
Separately test the deployed delivery adapter with a mailbox you control. A local template test cannot detect an absent production secret or a blocked outbound connection. Exercise failure handling so errors do not disappear behind fail_silently=True.
| Failure | What to verify |
|---|---|
| Missing configuration | The feature fails clearly without exposing secrets |
| Duplicate job | One intended notification identity is preserved |
| Unknown submission outcome | The system reconciles before blindly resending |
| Final recipient rejection | Delivery status and suppression handling update |
Give users an accurate status
Use wording that matches the stage: queued, submitted or delivery failed. Avoid claiming that a message is in the inbox just because the send helper returned a positive count. Receiving-server acceptance and mailbox placement require different evidence.
Our email idempotency guide covers retry design, while the MailBlastr blog includes delivery operations topics. Keep those responsibilities around the Django helper so email remains reliable when the application grows beyond its first successful test send.