Deployment
Email delivery

Configure Django Email Sending in Production

Send Django email reliably in production: SMTP TLS settings, sender addresses, SPF, DKIM and DMARC, timeouts, background sending, and tests.

Problem statement

Password resets, account verification, invoices, and error notifications all depend on Django being able to send email. In development, the console backend prints messages to the terminal, so it is easy to reach production without ever testing real delivery. Common results:

  • password reset emails never arrive because the cloud provider blocks outbound port 25
  • messages land in spam because the sending domain has no SPF, DKIM, or DMARC records
  • a slow SMTP server makes form submissions hang until Gunicorn kills the worker
  • error emails to admins silently fail because SERVER_EMAIL is root@localhost
  • both EMAIL_USE_TLS and EMAIL_USE_SSL are set, and Django refuses to send

This guide configures Django to send through a transactional email provider over authenticated SMTP, sets up domain authentication, moves sending out of the request path, and tests the result.

How a Django email reaches an inboxDjangosend_mail()SMTP relayprovider on port 587Receiving serverSPF, DKIM, DMARCInboxor spam folderSettings get the message accepted; DNS authentication decides whether it is trusted.

Quick answer

  1. Use a transactional email provider (SMTP relay or HTTP API), not a mail server on your web host.
  2. Configure EMAIL_HOST, EMAIL_PORT = 587, EMAIL_USE_TLS = True, EMAIL_HOST_USER, and EMAIL_HOST_PASSWORD from environment variables.
  3. Set EMAIL_TIMEOUT so a slow SMTP server cannot hang workers.
  4. Set DEFAULT_FROM_EMAIL and SERVER_EMAIL to addresses on your verified domain.
  5. Publish SPF, DKIM, and DMARC DNS records for the sending domain.
  6. Send non-critical emails from a background task queue.
  7. Test with python manage.py sendtestemail and check the message headers in a real inbox.

Step-by-step solution

1. Choose a sending method

Running your own SMTP server for application email means managing IP reputation, bounces, and blocklists. Most cloud providers also restrict outbound port 25 by default. A transactional provider solves both. Options include Amazon SES, Postmark, Mailgun, SendGrid, and Brevo; all offer SMTP relay on port 587 and an HTTP API.

SMTP works with Django out of the box. HTTP APIs are usually faster and give better error details; the django-anymail package provides Django email backends for many providers if you prefer that route.

who actually talks to the recipient's mail server?How does Django hand mail to the internet?own mail serverPostfix on the web hostreputation, blocklistsprovider via SMTPAuthenticated relay :587works out of the boxprovider HTTP APIAPI email backendfaster, clearer errorsEither provider route is fine; running your own mail server is the one to avoiddjango-deployment.com
Transactional providers already carry the IP reputation that inbox placement depends on.

2. Configure SMTP settings

import os

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = os.environ["EMAIL_HOST"]
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587"))
EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"]
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "true").lower() == "true"
EMAIL_USE_SSL = os.environ.get("EMAIL_USE_SSL", "false").lower() == "true"
EMAIL_TIMEOUT = 10

DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "Example App <no-reply@example.com>")
SERVER_EMAIL = os.environ.get("SERVER_EMAIL", "alerts@example.com")

Port and encryption must match:

PortSettingBehavior
587EMAIL_USE_TLS = TruePlain connection upgraded with STARTTLS
465EMAIL_USE_SSL = TrueTLS from the first byte (implicit TLS)

EMAIL_USE_TLS and EMAIL_USE_SSL are mutually exclusive; setting both raises an error. Using port 465 with EMAIL_USE_TLS or port 587 with EMAIL_USE_SSL usually results in a timeout or an SSL error.

EMAIL_TIMEOUT defaults to None, meaning Django waits indefinitely on a stalled SMTP connection. Ten seconds is a reasonable ceiling for a relay.

3. Sender addresses

  • DEFAULT_FROM_EMAIL is used by send_mail() calls without an explicit sender and by features such as password reset.
  • SERVER_EMAIL is the sender of error emails to ADMINS and MANAGERS. Its default is root@localhost, which providers reject.

Both addresses must use a domain you have verified with the email provider. Using a free mailbox domain (for example a Gmail address) as the sender fails DMARC checks at the receiving side.

4. Admin error emails

ADMINS = [("Ops", "ops@example.com")]

Recent Django versions also accept a plain list of email addresses and deprecate the tuple format; check the release notes of the version you run and use the format it documents. Error emails are sent by the mail_admins logging handler when DEBUG = False. For busy sites, an error tracker such as Sentry handles grouping and rate limiting better than one email per exception.

5. Domain authentication with DNS

Receiving mail servers decide whether to trust your messages largely from three DNS records. Your provider's dashboard shows the exact values; the shapes look like this:

; SPF: which servers may send for example.com
example.com.            TXT  "v=spf1 include:provider-spf-domain.example ~all"

; DKIM: public key used to verify message signatures
selector1._domainkey.example.com.  CNAME  selector1.provider-dkim.example.

; DMARC: policy when SPF/DKIM alignment fails, and where to send reports
_dmarc.example.com.     TXT  "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"
  • A domain may have only one SPF record. If you already have one, add the provider's include: to it rather than creating a second record.
  • Start DMARC with p=none to collect reports, then move to quarantine or reject once legitimate mail passes.
  • Consider sending application mail from a subdomain such as mail.example.com to separate its reputation from your staff mailboxes.
three records decide trustSPF includes the providerone SPF record onlyDKIM records verified by the providerd= is your domainDMARC published with p=none firstread reports, then enforceApp mail on a subdomainmail.example.comA second SPF record addedbreaks SPF entirelyCheck the headers of a real received message for spf, dkim, and dmarc passdjango-deployment.com
Authentication gets mail considered; bounce handling and content keep it in the inbox.

Check records after publishing:

dig +short TXT example.com
dig +short TXT _dmarc.example.com
dig +short CNAME selector1._domainkey.example.com

6. Send outside the request

SMTP conversations take hundreds of milliseconds or more. Sending inside a view slows responses and ties success of the request to the email provider's availability. With Celery:

# accounts/tasks.py
from celery import shared_task
from django.core.mail import send_mail


@shared_task(bind=True, autoretry_for=(Exception,), retry_backoff=True, max_retries=5)
def send_welcome_email(self, user_email):
    send_mail(
        subject="Welcome to Example App",
        message="Thanks for signing up.",
        from_email=None,
        recipient_list=[user_email],
    )
the email leaves after the responseSignup view saves userin a transactionon_commit enqueuestaskrow now existsResponse returnedno SMTP waitWorker sends emailretries on failureA slow provider now delays a background task, not the userdjango-deployment.com
Enqueueing after commit guarantees the worker can find the user it is emailing.
# accounts/views.py
from django.db import transaction

from .tasks import send_welcome_email


def signup(request):
    # ... create user ...
    transaction.on_commit(lambda: send_welcome_email.delay(user.email))

transaction.on_commit queues the task only after the user row is committed, so the worker never looks up a user that does not exist yet. Retries with backoff handle temporary provider errors.

Password reset emails sent by Django's built-in views are synchronous. That is usually acceptable because they are infrequent; if not, override the form's send_mail method to queue a task.

7. Test sending

Django includes a management command for this:

python manage.py sendtestemail you@example.com
python manage.py sendtestemail --admins

Run it with the production environment loaded. Then open the received message's original source and check the authentication results header:

Authentication-Results: mx.example.net;
       dkim=pass header.d=example.com;
       spf=pass smtp.mailfrom=bounces.example.com;
       dmarc=pass header.from=example.com

dkim=pass and dmarc=pass for your domain are the important results. Also confirm error emails work by triggering a controlled exception in a staging environment.

8. Debugging connection problems

Test the network path from the server:

nc -vz smtp.provider.example 587
openssl s_client -starttls smtp -connect smtp.provider.example:587 -crlf -quiet </dev/null
connection errors, decodedErrorUsual causeTimed outport blocked by the host or cloudWRONG_VERSION_NUMBERSSL on 587, or TLS on 465Authentication failedwrong or unencoded credentialsConnection refusedwrong host or portTest from the server with openssl s_client before changing Djangodjango-deployment.com
Many clouds block port 25 outbound; use 587.

A timeout usually means outbound traffic to that port is blocked by a firewall or the hosting provider. A certificate or protocol error usually means the port and TLS mode do not match.

Verification check:

  • sendtestemail completes without an exception and the message arrives in the inbox, not spam
  • headers show SPF, DKIM, and DMARC passing
  • --admins delivers to the addresses in ADMINS
  • a signup in staging queues a task and the worker log shows the email sent

Explanation

Email delivery has two independent parts: getting the message accepted by your provider (SMTP settings and credentials) and getting it trusted by the recipient (domain authentication and reputation). Django handles the first with a handful of settings; DNS records handle the second. Moving sending into a task queue separates both from the user's request, so provider slowness or outages do not become application errors.

Edge cases / notes

Sandbox or restricted accounts

New Amazon SES accounts start in a sandbox that only allows sending to verified addresses. Other providers have similar approval steps. Test with an unverified external address before launch.

Bounces and complaints

Configure your provider to process bounces and complaints, and stop sending to addresses that hard bounce. Continuing to send damages reputation.

Staging environments

Use a separate provider project or a test inbox service for staging, so test emails never reach real users.

Troubleshooting common SMTP errors

Django raises exceptions from smtplib when sending fails. The exception type points to the layer at fault:

ExceptionMeaningTypical fix
TimeoutError or socket.timeoutno connection to the SMTP hostoutbound port blocked; try 587 or 2525 if the provider supports it
ssl.SSLError: wrong version numberTLS mode does not match the port587 with EMAIL_USE_TLS, 465 with EMAIL_USE_SSL
SMTPAuthenticationError (535)wrong username or passworduse provider SMTP credentials, not your dashboard login
SMTPSenderRefused (550, 553)sender domain not verifiedverify the domain; fix DEFAULT_FROM_EMAIL
SMTPRecipientsRefusedrecipient rejected, often sandbox restrictionsleave sandbox mode or verify the recipient
SMTPDataError (554)message rejected, for example content or reputationcheck provider activity logs for the rejection reason

fail_silently=True hides all of these. Keep it False for transactional mail sent from tasks, so failures are logged and retried.

Logging email attempts

Wrap sending in a small helper that logs outcomes without message bodies:

import logging
from smtplib import SMTPException

from django.core.mail import send_mail

logger = logging.getLogger(__name__)


def send_transactional(subject, body, recipient):
    try:
        sent = send_mail(subject, body, None, [recipient])
    except (SMTPException, OSError):
        logger.exception("email send failed subject=%r", subject)
        raise
    logger.info("email sent subject=%r count=%s", subject, sent)
    return sent

Avoid logging recipient addresses in systems where logs are widely accessible; they are personal data.

HTML emails and templates

Send multipart messages with a plain text part and an HTML part; spam filters penalize HTML-only messages:

from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string


def send_receipt(order):
    context = {"order": order}
    text = render_to_string("emails/receipt.txt", context)
    html = render_to_string("emails/receipt.html", context)
    message = EmailMultiAlternatives("Your receipt", text, None, [order.email])
    message.attach_alternative(html, "text/html")
    message.send()

Absolute URLs in emails must use your production domain. Build them from a setting such as SITE_URL rather than from request.build_absolute_uri() inside background tasks, which have no request.

Common mistakes

  • Using a personal Gmail account's SMTP for application email; sending limits and account security features block it quickly.
  • Setting DEFAULT_FROM_EMAIL to a domain without SPF or DKIM.
  • Sending password reset emails to addresses entered in the form without rate limiting, which enables abuse.
  • Forgetting that staging uses real SMTP credentials and emails real users.

Next steps

FAQ

Should I use EMAIL_USE_TLS or EMAIL_USE_SSL in Django?

Use EMAIL_USE_TLS = True with port 587 (STARTTLS), or EMAIL_USE_SSL = True with port 465 (implicit TLS). Never enable both.

Why do Django emails go to spam?

The sending domain is usually missing SPF, DKIM, or DMARC records, or the from address uses a domain you do not control. Verify the domain with your provider and publish its DNS records.

How do I test Django email settings in production?

Run python manage.py sendtestemail you@example.com with the production settings loaded, then check delivery and the authentication results in the message headers.

Why does sending email make my Django view slow?

The SMTP conversation happens during the request. Send from a background task queue after the transaction commits, and set EMAIL_TIMEOUT so stalled connections fail quickly.

Keep going