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.

What inbox providers checkSPFallowed sendersDKIMsigned by your domainDMARCalignment and policyReputationbounces, complaintsAuthentication gets mail considered; reputation and content decide the inbox or spam folder.
  1. Send through a dedicated transactional email provider, not the app server's own mail server.
  2. Publish SPF, DKIM, and DMARC for your sending domain, and make sure DKIM and SPF align with the From domain.
  3. Use a real sender on your domain (DEFAULT_FROM_EMAIL), with a reply address that reaches a human or is clearly no-reply.
  4. Process bounces and complaints and stop sending to failing addresses.
  5. Keep messages simple: correct links to your real domain, plain-text alternative, no URL shorteners.
  6. 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_BACKEND points at the provider (SMTP with TLS on port 587, or an API backend), with EMAIL_TIMEOUT set.
  • Staging and development cannot email real users (console backend, sandbox, or allowlisted recipients).

2. Domain authentication

  • SPF: a single SPF TXT record on the domain used in the envelope sender (return path) includes your provider:
example.com.  TXT  "v=spf1 include:_spf.provider.example ~all"
three records, three jobsProvesDNS recordSPFthis server may send for the domainTXT on the domainDKIMthe message was not alteredTXT at selector._domainkeyDMARCwhat to do when checks failTXT at _dmarcStart DMARC at p=none, read the reports, then tightendjango-deployment.com
Major mailbox providers now reject bulk mail that lacks all three.
  • 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 From domain 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_EMAIL uses 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.com from an unconfigured Sites record or localhost.
  • Messages include a plain-text part (Django's EmailMultiAlternatives or send_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.
from bounce to suppressionMessage sentvia the providerHard bouncemailbox does not existProvider webhooksigned POST to DjangoAddress suppressedflag on the userNo further sendsreputation protectedRepeatedly mailing dead addresses damages the whole domaindjango-deployment.com
Verify the webhook signature, or anyone can suppress your users’ addresses.

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).
send mail outside the requestsend_mail() in the view✕SMTP latency added to the response✕provider outage becomes a 500✕no retryTask queue✓request returns immediately✓retries with backoff✓enqueued with on_commitSet EMAIL_TIMEOUT either way, so a hung server cannot hang a workerdjango-deployment.com
Password reset mail is the exception many teams keep synchronous, with a timeout.

6. Verification and monitoring

  • Test messages to major providers (Gmail, Outlook, Yahoo) show spf=pass, dkim=pass, and dmarc=pass in 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

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.

Keep going