Django Email Deliverability Checklist
Make Django's transactional email reach inboxes: a reputable provider, SPF, DKIM, and DMARC alignment, sender addresses, bounce and complaint handling, content hygiene, background sending, and monitoring.
Problem statement
Django sends the email and reports success, but users say the password reset never arrived. It is in spam, delayed, or silently dropped by the receiving provider. Sending is the easy part; deliverability depends on authentication records in DNS, the reputation of the sending infrastructure, how bounces and complaints are handled, and what the message looks like.
This checklist covers deliverability for transactional email from a Django app: account emails, receipts, notifications. It assumes a dedicated email provider (SES, Postmark, Mailgun, SendGrid, or similar); sending directly from an application server's IP rarely achieves good inbox placement.
- Send through a dedicated transactional email provider, not the app server's own mail server.
- Publish SPF, DKIM, and DMARC for your sending domain, and make sure DKIM and SPF align with the
Fromdomain. - Use a real sender on your domain (
DEFAULT_FROM_EMAIL), with a reply address that reaches a human or is clearly no-reply. - Process bounces and complaints and stop sending to failing addresses.
- Keep messages simple: correct links to your real domain, plain-text alternative, no URL shorteners.
- Send from background tasks with retries, and monitor delivery, bounce, and complaint rates.
Checklist
1. Provider and sending path
- A transactional email provider is used, with the account verified for production sending (out of any sandbox).
- Transactional and marketing email use separate streams, subdomains, or providers, so marketing complaints do not hurt password reset delivery.
- Django's
EMAIL_BACKENDpoints at the provider (SMTP with TLS on port 587, or an API backend), withEMAIL_TIMEOUTset. - Staging and development cannot email real users (console backend, sandbox, or allowlisted recipients).
2. Domain authentication
- SPF: a single SPF
TXTrecord on the domain used in the envelope sender (return path) includes your provider:
example.com. TXT "v=spf1 include:_spf.provider.example ~all"
- There is only one SPF record per name, and it stays under the limit of 10 DNS lookups.
- DKIM: the provider's DKIM records (CNAME or TXT) are published and verified for your domain; messages are signed with
d=example.com(or a subdomain). - DMARC: a DMARC record exists, starting with monitoring and moving to enforcement once reports are clean:
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"
- Alignment: the
Fromdomain matches the DKIM signing domain (and ideally the return path domain via a custom MAIL FROM or bounce domain). Alignment is what DMARC checks. - Large mailbox providers require authentication and, for bulk senders, DMARC and easy unsubscribe for marketing mail; transactional mail should meet the same authentication standards.
3. Sender identity and content
DEFAULT_FROM_EMAILuses your domain and a recognisable name:"Example <no-reply@example.com>".SERVER_EMAIL(error emails to admins) uses your domain too.- Links in emails use your production domain over HTTPS, not
example.comfrom an unconfigured Sites record orlocalhost. - Messages include a plain-text part (Django's
EmailMultiAlternativesorsend_mail(..., html_message=...)send both). - No URL shorteners, no link-only messages, and images are not the only content.
- Subject lines and content are specific and expected by the recipient (they just requested a reset, placed an order).
4. Bounces and complaints
- The provider's bounce and complaint notifications (webhooks or queues) are processed by the app.
- Hard bounces mark addresses as undeliverable; the app stops sending to them.
- Complaints (spam reports) stop further non-essential email to that address.
- The provider's suppression list is enabled.
- Bounce and complaint rates are monitored against the provider's thresholds; sustained high rates can suspend sending.
5. Sending mechanics
- Emails are sent from background tasks (Celery or another worker) enqueued with
transaction.on_commit, so slow providers do not delay requests and failures are retried. - Retries use backoff and a limit, and permanent errors (invalid address) are not retried.
- Sending volume increases gradually on new domains or IPs (warm-up), especially for dedicated IPs.
- Idempotency prevents duplicate emails when tasks retry (record that an email was sent).
6. Verification and monitoring
- Test messages to major providers (Gmail, Outlook, Yahoo) show
spf=pass,dkim=pass, anddmarc=passin their headers. - DMARC aggregate reports are received and reviewed (a reporting service makes them readable).
- Delivery, open (if tracked), bounce, and complaint metrics are visible in the provider's dashboard or your monitoring.
- An end-to-end check periodically triggers a real email (for example a password reset for a test account) and confirms it arrives.
Explanation
Receiving providers decide what to do with each message based on whether it is authenticated (SPF and DKIM prove the sending infrastructure and domain are authorised, DMARC ties them to the visible From address), the reputation of the domain and IP (built from past bounces, complaints, and engagement), and the content. A Django app influences all three: DNS records and provider configuration handle authentication, bounce and complaint processing protects reputation, and templates determine content. Missing any one of them lowers inbox placement for every email you send.
Edge cases / notes
Shared versus dedicated IPs
Most transactional providers send from shared IP pools with managed reputation, which is best for low and medium volumes. Dedicated IPs only help at high, steady volume and require warm-up.
Subdomains
Sending from mail.example.com or notify.example.com separates reputation from your main domain. Use it consistently in From, DKIM, and return path.
When to automate this
Manage DNS records as code, process bounces through a tested webhook, and run the end-to-end email check from your monitoring on a schedule. Alert when bounce or complaint rates rise, before the provider acts.
Next steps
- Configure Django's backends with Configure Django Email Sending in Production.
- Set up a provider end to end with Send Django Email with Amazon SES.
- If messages do not send at all, see Fix Django Emails Not Sending in Production.
- If links in emails point to the wrong domain, see Fix Django Links Using example.com (Sites Framework).
FAQ
Why do Django emails go to spam?
Usually missing or misaligned SPF, DKIM, or DMARC, sending from an IP without reputation, or content that looks suspicious. Send through a provider with authenticated domain records.
Do I need SPF, DKIM, and DMARC for transactional email?
Yes. Major mailbox providers expect all three, and DMARC alignment ties authentication to the address users see.
Should password reset emails come from a no-reply address?
It is common. Use your domain, a clear name, and consider a monitored reply address, since users do reply to transactional email.
How do I handle bounced emails in Django?
Process the provider's bounce notifications, mark addresses as undeliverable, and stop sending to them.