Back to Blog

How to Send Emails in Django (2026 Guide)

13 min read

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

Terminal
pip install httpx
Terminal
pip install resend
Terminal
pip install sendgrid

Custom Email Backend

This is the Django way. Create a backend and all your existing send_mail() calls automatically use the API provider.

myapp/backends.py
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 sent
myapp/backends.py
import 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 sent
myapp/backends.py
from 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 sent

Configure in settings.py:

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>"
settings.py
import os

EMAIL_BACKEND = "myapp.backends.ResendBackend"
RESEND_API_KEY = os.environ["RESEND_API_KEY"]
DEFAULT_FROM_EMAIL = "Your App <noreply@yourdomain.com>"
settings.py
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

  1. Custom email backends to plug providers into Django's email system
  2. Django templates for HTML emails
  3. Celery for background sending
  4. Built-in test backend for testing
  5. Per-environment config for dev/staging/production

Pick your provider, create the backend, and start sending.