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_EMAILisroot@localhost - both
EMAIL_USE_TLSandEMAIL_USE_SSLare 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.
Quick answer
- Use a transactional email provider (SMTP relay or HTTP API), not a mail server on your web host.
- Configure
EMAIL_HOST,EMAIL_PORT = 587,EMAIL_USE_TLS = True,EMAIL_HOST_USER, andEMAIL_HOST_PASSWORDfrom environment variables. - Set
EMAIL_TIMEOUTso a slow SMTP server cannot hang workers. - Set
DEFAULT_FROM_EMAILandSERVER_EMAILto addresses on your verified domain. - Publish SPF, DKIM, and DMARC DNS records for the sending domain.
- Send non-critical emails from a background task queue.
- Test with
python manage.py sendtestemailand 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.
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:
| Port | Setting | Behavior |
|---|---|---|
| 587 | EMAIL_USE_TLS = True | Plain connection upgraded with STARTTLS |
| 465 | EMAIL_USE_SSL = True | TLS 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_EMAILis used bysend_mail()calls without an explicit sender and by features such as password reset.SERVER_EMAILis the sender of error emails toADMINSandMANAGERS. Its default isroot@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=noneto collect reports, then move toquarantineorrejectonce legitimate mail passes. - Consider sending application mail from a subdomain such as
mail.example.comto separate its reputation from your staff mailboxes.
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],
)
# 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
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:
sendtestemailcompletes without an exception and the message arrives in the inbox, not spam- headers show SPF, DKIM, and DMARC passing
--adminsdelivers to the addresses inADMINS- 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:
| Exception | Meaning | Typical fix |
|---|---|---|
TimeoutError or socket.timeout | no connection to the SMTP host | outbound port blocked; try 587 or 2525 if the provider supports it |
ssl.SSLError: wrong version number | TLS mode does not match the port | 587 with EMAIL_USE_TLS, 465 with EMAIL_USE_SSL |
SMTPAuthenticationError (535) | wrong username or password | use provider SMTP credentials, not your dashboard login |
SMTPSenderRefused (550, 553) | sender domain not verified | verify the domain; fix DEFAULT_FROM_EMAIL |
SMTPRecipientsRefused | recipient rejected, often sandbox restrictions | leave sandbox mode or verify the recipient |
SMTPDataError (554) | message rejected, for example content or reputation | check 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_EMAILto 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
- If messages still do not arrive, follow Fix Django Emails Not Sending in Production.
- Set up background workers with Configure Redis and Celery for Django Production.
- Store SMTP credentials properly with Manage Django Secrets in Production.
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.