Sending email with SendGrid on Heroku for a Django app
To send email from a Django app on Heroku with SendGrid in 2026, create a SendGrid API key, store it as a Heroku config var (SENDGRID_API_KEY), and point Django's email backend at SendGrid over SMTP — using the literal username apikey and the key itself as the password. Then authenticate your sending domain (SPF + DKIM) so messages actually land in the inbox.
The minimum working SMTP configuration is just a few settings:
# settings.py
import os
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.sendgrid.net"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = "apikey" # the literal word "apikey"
EMAIL_HOST_PASSWORD = os.environ["SENDGRID_API_KEY"] # the real API key
DEFAULT_FROM_EMAIL = "no-reply@yourdomain.com"
The rest of this guide covers what production senders need: getting SendGrid onto Heroku, the cleaner django-anymail setup, SMTP vs the Web API, and the domain authentication and 2024 Gmail/Yahoo rules that decide whether your mail is delivered. At MicroPyramid we have shipped Django email and notification flows for 12+ years across 50+ projects, so these patterns reflect what survives contact with real inboxes.
Key takeaways
- Use an API key, not a username/password. Authenticate to SendGrid with an API key; for SMTP the username is the literal string
apikeyand the password is the key. - Never hardcode the key. Read it from
os.environ["SENDGRID_API_KEY"]and set it as a Heroku config var. - Heroku is no longer free. Free dynos and free Postgres were removed in November 2022 — running on Heroku now requires a paid plan.
- Two clean integration paths: Django's built-in SMTP
EMAIL_BACKEND, or the better-maintained django-anymail library (SMTP or the Web API). - Deliverability is the hard part. Authenticate your sending domain with SPF + DKIM (and ideally DMARC); SendGrid "Single Sender" verification is for testing only.
- Gmail/Yahoo 2024 bulk-sender rules require SPF, DKIM, DMARC and one-click unsubscribe once you send at volume.
Is Heroku still free in 2026?
No. Heroku removed its free dynos and free Postgres tier in November 2022, so deploying and running a Django app on Heroku today means a paid Eco/Basic dyno (or higher) plus a paid database plan. This is the single biggest change since this article was first written — any tutorial that tells you to "just deploy free on Heroku" is out of date.
If you are setting up the deployment side from scratch, see our companion walkthrough on how to deploy a Django app on Heroku for the buildpack, Procfile, and config-var basics. Everything below assumes your Django app already runs on a Heroku dyno.
How do I add SendGrid to Heroku?
You have two options:
- The Heroku SendGrid add-on still exists and provisions a SendGrid account attached to your app. You can add it from the Heroku Dashboard (Resources -> Add-ons) or the CLI. Historically it set
SENDGRID_USERNAME/SENDGRID_PASSWORDconfig vars — do not use those for auth; create an API key instead. - Bring your own SendGrid (Twilio SendGrid) account (recommended for most teams), create an API key under Settings -> API Keys with "Mail Send" permission, and expose it to your dyno as a config var.
The modern Heroku CLI is installed with curl https://cli-assets.heroku.com/install.sh | sh (the old toolbelt installer is long deprecated). Whichever route you choose, the key lives in a config var, never in your code or Git history:
# Optional: provision the SendGrid add-on (creates a managed SendGrid account)
heroku addons:create sendgrid:starter -a your-app
# Recommended: create an API key in the SendGrid dashboard, then store it
# as a Heroku config var that Django reads via os.environ.
heroku config:set SENDGRID_API_KEY="SG.xxxxxxxxxxxxxxxxxxxxxx" -a your-app
heroku config:set DEFAULT_FROM_EMAIL="no-reply@yourdomain.com" -a your-app
# Verify it is set (value is injected into the dyno environment at runtime)
heroku config:get SENDGRID_API_KEY -a your-appSMTP relay vs the Web API: which should I use?
SendGrid offers two ways to send: the SMTP relay (smtp.sendgrid.net) and the Web API v3 (HTTPS). Django's built-in email works over SMTP out of the box; the Web API needs a library such as the official sendgrid SDK or, more conveniently, django-anymail, which plugs SendGrid into Django's normal email API.
| SMTP relay | Web API (django-anymail) | |
|---|---|---|
| Django config | Built-in EMAIL_BACKEND |
anymail app + backend |
| Auth | Username apikey + API key |
API key only |
| Extra dependency | None | django-anymail[sendgrid] |
| Speed / overhead | Extra SMTP round-trips | Single HTTPS call |
| Tags, metadata, templates | Limited | Full (tags, metadata, dynamic templates) |
| Delivery events / webhooks | Harder to wire up | First-class support |
| Best for | Quick setup, low volume | Production, higher volume, analytics |
For a simple contact form, SMTP is fine. For anything with templates, tagging, or event tracking, use the Web API via django-anymail.
The apikey username gotcha (SMTP)
The most common reason SMTP auth fails with SendGrid is the username. When using the SMTP relay you do not put your SendGrid login email in EMAIL_HOST_USER. The username is the literal, lowercase string apikey for every account, and the password is your actual API key (the long SG.… value). Get this backwards and SendGrid returns a 535 authentication error.
Option A: Django SMTP backend with environment variables
This uses Django's built-in SMTP backend — zero extra dependencies. Read the API key from the environment (the Heroku config var you set earlier) so the secret never touches source control.
# settings.py
import os
# Django's built-in SMTP backend talking to SendGrid's relay.
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.sendgrid.net"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
# Auth: the username is the LITERAL string "apikey", not your email.
EMAIL_HOST_USER = "apikey"
EMAIL_HOST_PASSWORD = os.environ["SENDGRID_API_KEY"]
# Use a from-address on a domain you have authenticated in SendGrid.
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "no-reply@yourdomain.com")
# Optional: fail fast in production if the key is missing.
if not os.environ.get("SENDGRID_API_KEY"):
raise RuntimeError("SENDGRID_API_KEY is not set")Now send mail with Django's ordinary email API — send_mail() for one-liners, or EmailMultiAlternatives when you need an HTML body:
from django.core.mail import send_mail, EmailMultiAlternatives
from django.conf import settings
# Simple plain-text send.
send_mail(
subject="Welcome to Acme",
message="Thanks for signing up!",
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=["user@example.com"],
fail_silently=False,
)
# HTML email with a plain-text fallback (better deliverability).
msg = EmailMultiAlternatives(
subject="Your receipt",
body="Thanks for your order. View it online for the formatted version.",
from_email=settings.DEFAULT_FROM_EMAIL,
to=["user@example.com"],
)
msg.attach_alternative("<h1>Thanks for your order</h1>", "text/html")
msg.send()Option B: django-anymail (recommended)
django-anymail is the well-maintained library that wires SendGrid (and other ESPs) into Django's standard email API. You keep using send_mail/EmailMessage, but gain the Web API: tags, metadata, SendGrid dynamic templates, and tracking/event webhooks. Install it with the SendGrid extra and configure the backend:
# Install (the [sendgrid] extra pulls in what the SendGrid backend needs)
pip install "django-anymail[sendgrid]"
# settings.py
import os
INSTALLED_APPS = [
# ...
"anymail",
]
EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend"
ANYMAIL = {
"SENDGRID_API_KEY": os.environ["SENDGRID_API_KEY"],
}
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "no-reply@yourdomain.com")# Plain Django send_mail() now goes through SendGrid's Web API:
from django.core.mail import send_mail
send_mail("Hello", "Body text", None, ["user@example.com"])
# Or use Anymail's message class for tags, metadata, and dynamic templates:
from anymail.message import AnymailMessage
message = AnymailMessage(
subject="Your weekly report",
body="Plain-text fallback body.",
to=["user@example.com"],
)
message.tags = ["weekly-report"] # for SendGrid analytics
message.metadata = {"user_id": "12345"} # echoed back in webhooks
message.esp_extra = {"asm": {"group_id": 1234}} # SendGrid unsubscribe group
message.send()
print(message.anymail_status.recipients["user@example.com"].status) # e.g. "queued"Why does my SendGrid email land in spam?
Almost always because the sending domain is not authenticated. This is the #1 deliverability problem, and no amount of Django config fixes it. Before you trust SendGrid in production:
- Authenticate your domain (SPF + DKIM). In SendGrid go to Settings -> Sender Authentication -> Authenticate Your Domain. SendGrid gives you CNAME records to add at your DNS provider; these publish SPF and DKIM so mailbox providers can verify the mail really came from you.
- Add a DMARC record. A TXT record at
_dmarc.yourdomain.com(start withp=noneto monitor, then tighten) tells receivers what to do with mail that fails SPF/DKIM alignment. - "Single Sender Verification" is for testing only. It verifies one from-address without authenticating the domain — fine for a quick demo, not for production volume.
- Meet the Gmail & Yahoo 2024 bulk-sender rules. Since February 2024, bulk senders (roughly 5,000+ messages/day to Gmail) must have SPF, DKIM and DMARC, send from an authenticated domain, keep the spam-complaint rate under ~0.3%, and include one-click unsubscribe (the
List-Unsubscribeheader) on marketing mail. Even below that threshold, following these rules improves inbox placement.
Send a couple of test messages and check the headers (Gmail's "Show original") report SPF: PASS, DKIM: PASS, and DMARC: PASS before going live.
SendGrid vs Amazon SES vs Mailgun on Heroku
SendGrid is a solid default, but it is not the only option. All three work from Django over SMTP or via django-anymail. A quick comparison (provider capabilities, not pricing):
| SendGrid | Amazon SES | Mailgun | |
|---|---|---|---|
| Best for | Fast setup, marketing + transactional, templates | High-volume transactional, AWS-native stacks | Developer-centric transactional + inbound routing |
| Heroku setup | Add-on or own account + API key | Own AWS account; IAM-scoped SMTP/API creds | Own account + API key |
| Django integration | SMTP or django-anymail |
SMTP, django-anymail, or django-ses |
SMTP or django-anymail |
| Deliverability tooling | Strong (auth wizard, analytics) | Solid; you own reputation + config | Strong (logs, suppression, analytics) |
| Inbound parsing | Inbound Parse webhook | Receiving via S3/Lambda rules | Routes / inbound webhooks |
If your stack already lives on AWS, sending through SES can be cheaper and simpler — see our guide to send and receive email with Amazon SES, and our AWS consulting services for help wiring it into your infrastructure.
Best practices and common gotchas
- Keep the key in config vars.
os.environ["SENDGRID_API_KEY"]on Heroku; never commit it. Rotate it immediately if it leaks. - Scope the API key. Give it only "Mail Send" permission, not full access.
- Send asynchronously. Don't block the web request on the email call — queue it in a background worker (for example Celery) so a slow API response or transient error retries one email instead of failing the user's request.
- Authenticate the from-domain and match
DEFAULT_FROM_EMAILto it; mismatches hurt DMARC alignment. - Use the sandbox / a console backend in development (
django.core.mail.backends.console.EmailBackend) so tests don't send real mail. - Handle inbound too. SendGrid can parse replies into your app via its Inbound Parse webhook — see parsing inbound email with SendGrid and Django.
Frequently Asked Questions
What username and password do I use for SendGrid SMTP in Django?
The username is the literal lowercase string apikey — the same for every account — set in EMAIL_HOST_USER. The password is your actual SendGrid API key (the long SG.… value), set in EMAIL_HOST_PASSWORD and read from an environment variable. Do not use your SendGrid login email as the username; that is the most common cause of a 535 authentication error.
Is Heroku still free for hosting a Django app?
No. Heroku discontinued its free dynos and free Postgres tier in November 2022, so any Django app on Heroku now needs a paid dyno (Eco/Basic or higher) and a paid database plan. The SendGrid integration itself is unchanged, but the days of running the whole stack for free are over.
Should I use Django's SMTP backend or django-anymail?
Use the built-in SMTP backend for quick, low-volume sending — it needs no extra dependency. Use django-anymail for production: it sends over SendGrid's Web API and adds tags, metadata, dynamic templates, and delivery-event webhooks while keeping Django's normal send_mail/EmailMessage API. Install it with pip install "django-anymail[sendgrid]" and set EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend".
Why is my SendGrid email going to spam?
Almost always because your sending domain is not authenticated. Set up SPF and DKIM via SendGrid's Authenticate Your Domain wizard (it provides CNAME records for your DNS), add a DMARC TXT record, and send from that authenticated domain. SendGrid's "Single Sender Verification" is for testing only and does not provide proper domain authentication, so production mail sent that way often gets filtered.
Do I need the Heroku SendGrid add-on?
No. The add-on still exists and is convenient because it provisions a SendGrid account tied to your app, but you can equally create your own (Twilio) SendGrid account, generate an API key, and set it with heroku config:set SENDGRID_API_KEY=.... Either way, authenticate with an API key — not the legacy SENDGRID_USERNAME/SENDGRID_PASSWORD config vars.
How do I keep my SendGrid API key secure on Heroku?
Store it as a Heroku config var and read it in Django with os.environ["SENDGRID_API_KEY"] — never hardcode it or commit it to Git. Scope the key to "Mail Send" only, rotate it if it is ever exposed in logs or screenshots, and use a separate key for staging versus production so you can revoke one without affecting the other.
Get transactional email working in production
Sending one test email is easy; reliable transactional email — authenticated domains, async delivery, bounce and event handling, and inbox placement at volume — is where teams get stuck. If you want SendGrid (or SES/Mailgun) wired cleanly into your Django app on Heroku or AWS, our Django development services team has built these flows across 50+ projects. Talk to us about your email and notification requirements.