How to Send Emails in Django (2026 Guide)

Django has a solid email system built in. django.core.mail handles the basics, and custom backends let you swap the transport layer without touching your application code. Most tutorials configure Gmail SMTP and stop there.
This guide shows you how to use Django's email system with API-based providers for production. Custom backends, HTML templates, Celery background jobs, and the email patterns every Django app needs.
Django's Email System
Django's built-in email API is clean:
from django.core.mail import send_mail
send_mail(
subject="Welcome",
message="Plain text fallback",
from_email=None, # uses DEFAULT_FROM_EMAIL
recipient_list=["user@example.com"],
html_message="<h1>Welcome!</h1>",
)The power is in backends. You can swap between SMTP, console output (for testing), or a custom backend that calls any API. Your application code stays the same.
Pick a Provider
- Sequenzy is built for SaaS. Transactional emails, marketing campaigns, automated sequences from one API. Native Stripe integration.
- Resend is developer-friendly. Clean API. They have one-off broadcast campaigns but no automations or sequences.
- SendGrid is the enterprise option. Good for high volume.
Install
pip install httpxpip install resendpip install sendgridCustom Email Backend
This is the Django way. Create a backend and all your existing send_mail() calls automatically use the API provider.
import httpx
from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
class SequenzyBackend(BaseEmailBackend):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.api_key = settings.SEQUENZY_API_KEY
self.client = httpx.Client(
base_url="https://api.sequenzy.com/v1",
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=30.0,
)
def send_messages(self, email_messages):
sent = 0
for message in email_messages:
try:
body = (
message.alternatives[0][0]
if hasattr(message, "alternatives") and message.alternatives
else message.body
)
self.client.post(
"/transactional/send",
json={
"to": message.to[0],
"subject": message.subject,
"body": body,
},
)
sent += 1
except httpx.HTTPError:
if not self.fail_silently:
raise
return sentimport resend
from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
class ResendBackend(BaseEmailBackend):
def __init__(self, **kwargs):
super().__init__(**kwargs)
resend.api_key = settings.RESEND_API_KEY
def send_messages(self, email_messages):
sent = 0
for message in email_messages:
try:
html = (
message.alternatives[0][0]
if hasattr(message, "alternatives") and message.alternatives
else f"<pre>{message.body}</pre>"
)
resend.Emails.send({
"from": settings.DEFAULT_FROM_EMAIL,
"to": message.to[0],
"subject": message.subject,
"html": html,
})
sent += 1
except Exception:
if not self.fail_silently:
raise
return sentfrom django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
class SendGridBackend(BaseEmailBackend):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
def send_messages(self, email_messages):
sent = 0
for message in email_messages:
try:
html = (
message.alternatives[0][0]
if hasattr(message, "alternatives") and message.alternatives
else f"<pre>{message.body}</pre>"
)
mail = Mail(
from_email=settings.DEFAULT_FROM_EMAIL,
to_emails=message.to[0],
subject=message.subject,
html_content=html,
)
self.sg.send(mail)
sent += 1
except Exception:
if not self.fail_silently:
raise
return sentConfigure in settings.py:
import os
EMAIL_BACKEND = "myapp.backends.SequenzyBackend"
SEQUENZY_API_KEY = os.environ["SEQUENZY_API_KEY"]
DEFAULT_FROM_EMAIL = "Your App <noreply@yourdomain.com>"import os
EMAIL_BACKEND = "myapp.backends.ResendBackend"
RESEND_API_KEY = os.environ["RESEND_API_KEY"]
DEFAULT_FROM_EMAIL = "Your App <noreply@yourdomain.com>"import os
EMAIL_BACKEND = "myapp.backends.SendGridBackend"
SENDGRID_API_KEY = os.environ["SENDGRID_API_KEY"]
DEFAULT_FROM_EMAIL = "noreply@yourdomain.com"Now all Django email functions work through your provider:
from django.core.mail import send_mail, EmailMultiAlternatives
# Simple text email
send_mail("Subject", "Body text", None, ["user@example.com"])
# HTML email
msg = EmailMultiAlternatives("Subject", "Fallback text", None, ["user@example.com"])
msg.attach_alternative("<h1>Hello!</h1>", "text/html")
msg.send()Django Templates for Emails
Use Django's template engine for email HTML:
<!-- templates/emails/welcome.html -->
<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; background: #f6f9fc; padding: 40px 0;">
<div style="max-width: 480px; margin: 0 auto; background: #fff; padding: 40px; border-radius: 8px;">
<h1 style="font-size: 24px;">Welcome, {{ name }}</h1>
<p style="color: #374151; line-height: 1.6;">Your account is ready.</p>
<a href="{{ login_url }}"
style="display:inline-block; background:#f97316; color:#fff; padding:12px 24px; border-radius:6px; text-decoration:none;">
Go to Dashboard
</a>
</div>
</body>
</html># myapp/emails.py
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
def send_welcome_email(user):
html = render_to_string("emails/welcome.html", {
"name": user.first_name or user.username,
"login_url": f"{settings.APP_URL}/dashboard",
})
msg = EmailMultiAlternatives(
subject=f"Welcome, {user.first_name}",
body="Welcome! Your account is ready.",
to=[user.email],
)
msg.attach_alternative(html, "text/html")
msg.send()Send from Views
# myapp/views.py
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from myapp.emails import send_welcome_email
@require_POST
def signup(request):
# ... create user
user = create_user(request.POST)
# Send welcome email
send_welcome_email(user)
return JsonResponse({"user": {"id": user.id, "email": user.email}})Background Sending with Celery
Don't send emails in the request cycle. Use Celery.
pip install celery redis# myapp/tasks.py
from celery import shared_task
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_welcome_email_task(self, user_id):
from django.contrib.auth import get_user_model
User = get_user_model()
try:
user = User.objects.get(id=user_id)
html = render_to_string("emails/welcome.html", {
"name": user.first_name,
"login_url": f"{settings.APP_URL}/dashboard",
})
msg = EmailMultiAlternatives(
subject=f"Welcome, {user.first_name}",
body="Welcome!",
to=[user.email],
)
msg.attach_alternative(html, "text/html")
msg.send()
except Exception as exc:
self.retry(exc=exc)# In your view
from myapp.tasks import send_welcome_email_task
def signup(request):
user = create_user(request.POST)
send_welcome_email_task.delay(user.id) # Non-blocking
return JsonResponse({"user": {"id": user.id}})Testing Emails
Django has a built-in test backend:
# settings/test.py
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"# tests/test_emails.py
from django.core import mail
from django.test import TestCase
class EmailTests(TestCase):
def test_welcome_email_sent(self):
send_welcome_email(self.user)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, f"Welcome, {self.user.first_name}")
self.assertIn(self.user.email, mail.outbox[0].to)Going to Production
1. Verify Your Domain
Add SPF, DKIM, DMARC records through your provider's dashboard.
2. Use Different Backends per Environment
# settings/development.py
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# settings/production.py
EMAIL_BACKEND = "myapp.backends.SequenzyBackend"
# settings/test.py
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"3. Always Use Celery in Production
Emails should never block HTTP responses. Queue everything.
Beyond Transactional
Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one API. Native Stripe integration for SaaS.
Wrapping Up
- Custom email backends to plug providers into Django's email system
- Django templates for HTML emails
- Celery for background sending
- Built-in test backend for testing
- Per-environment config for dev/staging/production
Pick your provider, create the backend, and start sending.