Send Django Email with Amazon SES
Send Django email through Amazon SES: domain verification with DKIM, leaving the sandbox, SMTP or django-ses API backends, IAM permissions, custom MAIL FROM, and bounce handling.
Problem statement
Password resets, receipts, and notifications have to arrive in inboxes, not spam folders. Sending directly from your server's IP rarely achieves that. Amazon Simple Email Service (SES) is an inexpensive, reliable way to send transactional email from Django, especially if the app already runs on AWS.
The setup has several steps that are easy to miss: new SES accounts start in a sandbox that only delivers to verified addresses, the domain needs DKIM records, SMTP credentials are not the same as IAM access keys, and bounces must be handled or SES may pause sending. This guide covers the path from a new account to production sending.
- In the SES console, create a domain identity for
example.comin the region you will send from, with Easy DKIM, and publish the three DKIM CNAME records. - Request production access to leave the sandbox.
- Choose a backend: SMTP (built into Django, needs SES SMTP credentials) or the
django-sesAPI backend (uses IAM credentials or an instance role). - Set
DEFAULT_FROM_EMAILandSERVER_EMAILto addresses on the verified domain. - Configure a custom MAIL FROM domain and a DMARC record for alignment.
- Handle bounces and complaints so you stop sending to bad addresses.
Step-by-step solution
1. Verify the domain
In the SES console (in your sending region, for example eu-west-1), create an identity of type Domain for example.com with Easy DKIM (RSA 2048). SES shows three CNAME records like:
abc123._domainkey.example.com CNAME abc123.dkim.amazonses.com
Publish all three. Verification usually completes within minutes once DNS resolves. Check from the CLI:
aws sesv2 get-email-identity --email-identity example.com --region eu-west-1 \
--query "{verified: VerifiedForSendingStatus, dkim: DkimAttributes.Status}"
SES is regional: identities, sending limits, and SMTP endpoints belong to one region. Send from the region where you verified the domain.
2. Leave the sandbox
In the sandbox, SES only sends to verified addresses and has a low daily quota. Request production access in the SES console's account dashboard, describing the email type (transactional), how recipients opt in, and how you handle bounces and complaints. Until approved, verify a few test recipient addresses so you can test end to end.
3a. Option A: SMTP backend
Create SMTP credentials in the SES console (SMTP settings → Create SMTP credentials). This creates an IAM user and gives you an SMTP username and password. The SMTP password is derived from the secret key; a normal IAM secret access key does not work as an SMTP password.
# settings/production.py
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "email-smtp.eu-west-1.amazonaws.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ["SES_SMTP_USERNAME"]
EMAIL_HOST_PASSWORD = os.environ["SES_SMTP_PASSWORD"]
EMAIL_TIMEOUT = 10
DEFAULT_FROM_EMAIL = "Example <no-reply@example.com>"
SERVER_EMAIL = "alerts@example.com"
Port 587 with STARTTLS is the usual choice. Some cloud providers block outbound port 25, so avoid it. EMAIL_TIMEOUT prevents a network problem from hanging a request indefinitely.
3b. Option B: SES API with django-ses
The API backend uses the AWS SDK, so it can authenticate with an EC2 instance profile, ECS task role, or other IAM role, with no static credentials in settings:
pip install django-ses
EMAIL_BACKEND = "django_ses.SESBackend"
AWS_SES_REGION_NAME = "eu-west-1"
AWS_SES_REGION_ENDPOINT = "email.eu-west-1.amazonaws.com"
DEFAULT_FROM_EMAIL = "Example <no-reply@example.com>"
SERVER_EMAIL = "alerts@example.com"
Grant the role only what it needs:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["ses:SendEmail", "ses:SendRawEmail"],
"Resource": "arn:aws:ses:eu-west-1:123456789012:identity/example.com"
}]
}
Check the django-ses documentation for your version's settings, including whether it uses the SES v2 API by default.
4. Send from a task, not the request
Even with SES, an SMTP or API call adds latency and can fail. Send transactional email from a background task (Celery or django.tasks) and enqueue it after the database transaction commits, so a slow provider never delays page loads and failures can be retried.
5. Align SPF and DMARC
DKIM signing with your domain is already in place. For SPF alignment, configure a custom MAIL FROM domain such as mail.example.com in the identity settings. SES asks you to publish:
mail.example.com MX 10 feedback-smtp.eu-west-1.amazonses.com
mail.example.com TXT "v=spf1 include:amazonses.com ~all"
Then publish a DMARC policy, starting in monitoring mode:
_dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
Move to p=quarantine or p=reject once reports show all legitimate mail passing.
6. Handle bounces and complaints
SES tracks your bounce and complaint rates and can pause sending if they get too high. Configure notifications for the identity (or a configuration set) to an SNS topic, and process them:
- Mark addresses that hard-bounce as undeliverable in your user table and stop emailing them.
- Treat complaints as unsubscribe requests.
django-ses includes an SNS webhook view and signals for bounce and complaint events. Also enable the account-level suppression list, which stops SES from sending to addresses that recently bounced or complained.
Verification check:
python manage.py sendtestemail you@example.com
python manage.py sendtestemail --admins
In the received message's headers, confirm dkim=pass, spf=pass, and dmarc=pass, and that the DKIM-Signature uses d=example.com. In the SES console, the account dashboard shows sends, bounces, and complaints.
Explanation
SES accepts your message over SMTP or HTTPS, signs it with your domain's DKIM key, and delivers it from Amazon's reputation-managed IP pools. Receiving servers check that the DKIM signature and SPF domain align with the From domain (DMARC), which is what separates your mail from spoofed mail. The sandbox and bounce monitoring exist because SES's reputation is shared; they are also good hygiene for your own domain's reputation.
Edge cases / notes
Multiple environments
Use a separate subdomain identity for staging (for example staging.example.com), or a console/file backend, so test emails cannot reach real users and staging bounces do not affect production reputation.
Error emails
SERVER_EMAIL is the sender for error emails to ADMINS. Floods of error emails during an incident can hit sending limits; an error tracker is a better primary channel.
When to automate this
Domain identity, DKIM records, MAIL FROM records, and the IAM policy can all be created with Terraform, so a new domain or environment is a code change. Bounce processing belongs in the application as a small, tested webhook.
Next steps
- See all backend options in Configure Django Email Sending in Production.
- If messages never arrive, use Fix Django Emails Not Sending in Production.
- Check deliverability end to end with the Django Email Deliverability Checklist.
- Store SMTP credentials safely with Manage Django Secrets in Production.
FAQ
Why does SES reject email to some recipients?
The account is still in the sandbox, where only verified recipient addresses are allowed. Request production access.
Can I use my AWS secret access key as the SES SMTP password?
No. SES SMTP passwords are derived from IAM credentials in a specific way. Create SMTP credentials in the SES console.
Should I use SMTP or the SES API with Django?
SMTP works with Django's built-in backend and any host. The API backend with django-ses can use IAM roles, which avoids storing credentials on AWS infrastructure.
Why does Gmail show "via amazonses.com"?
The message is not aligned with your domain. Enable Easy DKIM for your domain and configure a custom MAIL FROM domain.