{"id":389,"date":"2026-09-10T02:23:16","date_gmt":"2026-09-10T07:53:16","guid":{"rendered":"https:\/\/photonconsole.com\/blog\/?p=389"},"modified":"2026-09-12T08:41:30","modified_gmt":"2026-09-12T14:11:30","slug":"django-email-configuration-for-production-applications","status":"publish","type":"post","link":"https:\/\/photonconsole.com\/blog\/django-email-configuration-for-production-applications\/","title":{"rendered":"Django Email Configuration for Production Applications"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Your Django app sends email perfectly in development. You deploy it, and the emails stop arriving. Password reset links never reach users, signup confirmations vanish, and <code>send_mail()<\/code> keeps returning <code>1<\/code> as though everything worked.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Django is doing exactly what you configured it to do. The problem is that the settings which work on a laptop are not the settings that work on a server, and Django&#8217;s defaults are deliberately designed for development rather than production.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers the full production setup: the <code>settings.py<\/code> values that matter, connection handling, asynchronous sending with Celery, the password reset flow, and the specific configuration mistakes that let Django email fail without raising anything.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quick Answer: How Do You Configure Email in Django?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Set the SMTP backend and connection details in <code>settings.py<\/code>, reading credentials from environment variables:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import os\n\nEMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'\nEMAIL_HOST = 'smtp.photonrelay.com'\nEMAIL_PORT = 587\nEMAIL_HOST_USER = os.environ&#91;'EMAIL_HOST_USER']\nEMAIL_HOST_PASSWORD = os.environ&#91;'EMAIL_HOST_PASSWORD']\nEMAIL_USE_TLS = True\nEMAIL_TIMEOUT = 10\nDEFAULT_FROM_EMAIL = 'Your App &lt;noreply@yourdomain.com&gt;'\nSERVER_EMAIL = 'errors@yourdomain.com'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>from django.core.mail import send_mail\n\nsend_mail(\n    subject='Your verification code',\n    message='Your code is 492019.',\n    from_email=None,          # falls back to DEFAULT_FROM_EMAIL\n    recipient_list=&#91;'user@example.com'],\n    fail_silently=False,\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The application code is rarely the problem. What decides whether a message reaches an inbox is the backend you selected, the host you authenticate against, and whether your domain is authorised to send. An authenticated relay such as <a href=\"https:\/\/www.photonconsole.com\/\">PhotonConsole<\/a> on port 587 removes most of the failure modes covered below.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Django&#8217;s Email Layer Works<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Django routes every outgoing message through a configurable backend. The backend decides what actually happens to the message: sent over SMTP, printed to the console, written to a file, or held in memory for tests.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/docs.djangoproject.com\/en\/stable\/topics\/email\/\" target=\"_blank\" rel=\"noopener\">official Django email documentation<\/a> lists five built-in backends. Only one of them sends anything.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Backend<\/th><th>What it does<\/th><th>Use in<\/th><\/tr><\/thead><tbody><tr><td><code>smtp.EmailBackend<\/code><\/td><td>Sends over SMTP<\/td><td>Production<\/td><\/tr><tr><td><code>console.EmailBackend<\/code><\/td><td>Prints the message to stdout<\/td><td>Local development<\/td><\/tr><tr><td><code>filebased.EmailBackend<\/code><\/td><td>Writes each message to a file<\/td><td>Local inspection<\/td><\/tr><tr><td><code>locmem.EmailBackend<\/code><\/td><td>Stores messages in memory<\/td><td>Automated tests<\/td><\/tr><tr><td><code>dummy.EmailBackend<\/code><\/td><td>Discards everything silently<\/td><td>Disabling mail entirely<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Django does not deliver email itself. It hands a correctly formatted message to whichever backend is configured, and the SMTP backend hands it to whatever host you named. If you want the underlying protocol explained first, see our guide to <a href=\"https:\/\/photonconsole.com\/blog\/what-is-smtp-direct-answer\/\">what SMTP is<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Django Email Fails in Production<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"577\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_44_15-PM-1024x577.png\" alt=\"\tDiagram of the four Django settings that cause email to fail silently: console backend, mismatched TLS and SSL, default from address and missing DNS records\" class=\"wp-image-391\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_44_15-PM-1024x577.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_44_15-PM-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_44_15-PM-767x432.png 767w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_44_15-PM-1536x865.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_44_15-PM.png 1671w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Four settings that let Django report a successful send while nothing is delivered.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Most email failures in Django come from configuration or authentication rather than defects in the framework. These five account for the majority.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. The Console Backend Is Still Active<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Most tutorials and starter templates set <code>EMAIL_BACKEND<\/code> to the console backend so development does not send real mail. If that setting reaches production, every message is printed to your application logs and nothing is sent. <code>send_mail()<\/code> still returns <code>1<\/code>, because from Django&#8217;s point of view the send succeeded.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the most common cause of &#8220;Django says it sent the email but nothing arrived&#8221;.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. EMAIL_USE_TLS and EMAIL_USE_SSL Are Both Set<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">These two settings are mutually exclusive, and Django raises an error if both are <code>True<\/code>. More commonly, one is set to match the wrong port. Use TLS with port 587, or SSL with port 465, and never mix the pairings.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. DEFAULT_FROM_EMAIL Was Never Changed<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Django&#8217;s default from address is <code>webmaster@localhost<\/code>. That address does not exist on your domain, so it fails DMARC alignment and gives bounces nowhere to go. Mailbox providers treat mail from a non-existent sender with suspicion, in line with <a href=\"https:\/\/support.google.com\/mail\/answer\/81126\" target=\"_blank\" rel=\"noopener\">Google&#8217;s sender guidelines<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. No Authentication Records on Your Domain<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Without SPF and DKIM, receiving servers cannot verify your application is allowed to send as your domain. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/spf-dkim-dmarc-explained-simply\/\">SPF, DKIM and DMARC<\/a> covers the records, and the free <a href=\"https:\/\/www.photonconsole.com\/email-deliverability-checker.php\">email deliverability checker<\/a> shows what your domain currently publishes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. Your Host Blocks Outbound SMTP<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">AWS, Google Cloud, DigitalOcean and Azure restrict outbound port 25 by default. AWS documents its <a href=\"https:\/\/repost.aws\/knowledge-center\/ec2-port-25-throttle\" target=\"_blank\" rel=\"noopener\">port 25 throttle removal process<\/a>, though removal does not solve the reputation problem underneath. Our breakdown of <a href=\"https:\/\/photonconsole.com\/blog\/smtp-connection-timeout\/\">SMTP connection timeouts<\/a> covers the diagnosis.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quick Fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Django Reports Success but No Email Arrives<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Print <code>settings.EMAIL_BACKEND<\/code> in production and confirm it ends in <code>smtp.EmailBackend<\/code><\/li>\n\n\n\n<li>Confirm only one of <code>EMAIL_USE_TLS<\/code> or <code>EMAIL_USE_SSL<\/code> is <code>True<\/code><\/li>\n\n\n\n<li>Check <code>DEFAULT_FROM_EMAIL<\/code> is a real address on your own domain<\/li>\n\n\n\n<li>Set <code>fail_silently=False<\/code> so exceptions surface instead of being swallowed<\/li>\n\n\n\n<li>Run a test send from <code>manage.py shell<\/code> to see the actual SMTP error<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step-by-Step: Production Email Setup<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Keep Credentials Out of settings.py<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Read every credential from the environment. Never commit an SMTP password to version control.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># settings.py\nimport os\n\nEMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'\nEMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.photonrelay.com')\nEMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))\nEMAIL_HOST_USER = os.environ&#91;'EMAIL_HOST_USER']\nEMAIL_HOST_PASSWORD = os.environ&#91;'EMAIL_HOST_PASSWORD']\nEMAIL_USE_TLS = True\nEMAIL_USE_SSL = False\nEMAIL_TIMEOUT = 10\n\nDEFAULT_FROM_EMAIL = 'Your App &lt;noreply@yourdomain.com&gt;'\nSERVER_EMAIL = 'errors@yourdomain.com'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>DEFAULT_FROM_EMAIL<\/code> is used for ordinary application mail. <code>SERVER_EMAIL<\/code> is used for error reports sent to <code>ADMINS<\/code>. Setting only the first leaves your error emails coming from <code>root@localhost<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Common Mistake<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Setting <code>EMAIL_PORT = 465<\/code> while leaving <code>EMAIL_USE_TLS = True<\/code>. Port 465 expects the connection to be encrypted from the first byte, which is <code>EMAIL_USE_SSL<\/code>. The mismatch produces a connection that hangs until <code>EMAIL_TIMEOUT<\/code> expires, with no error explaining why. Use 587 with TLS, or 465 with SSL, and never set both flags.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Always Set EMAIL_TIMEOUT<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Without a timeout, a slow or unresponsive mail server can block a worker process indefinitely. Ten seconds is a reasonable default for a web request.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Verify the Connection Before Trusting It<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Test from the shell on the production server, not from your laptop.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py shell<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>from django.core.mail import send_mail\nfrom django.conf import settings\n\nprint(settings.EMAIL_BACKEND)   # confirm this is the SMTP backend\nprint(settings.EMAIL_HOST, settings.EMAIL_PORT)\n\nsend_mail(\n    'SMTP connection test',\n    'If you received this, the connection works.',\n    None,\n    &#91;'you@yourdomain.com'],\n    fail_silently=False,\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If this raises an exception, the problem is configuration or connectivity. If it returns <code>1<\/code> and nothing arrives, the problem is delivery \u2014 authentication records or filtering rather than Django.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Reuse One Connection for Multiple Messages<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Calling <code>send_mail()<\/code> in a loop opens a new SMTP connection for every message. Open one connection and send through it instead.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from django.core.mail import get_connection, EmailMultiAlternatives\n\ndef send_batch(messages):\n    connection = get_connection()\n    connection.open()\n\n    emails = &#91;]\n    for msg in messages:\n        email = EmailMultiAlternatives(\n            subject=msg&#91;'subject'],\n            body=msg&#91;'text'],\n            to=&#91;msg&#91;'to']],\n            connection=connection,\n        )\n        email.attach_alternative(msg&#91;'html'], 'text\/html')\n        emails.append(email)\n\n    connection.send_messages(emails)\n    connection.close()<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Send HTML With a Plain Text Alternative<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">HTML-only messages score worse with spam filters. <code>EmailMultiAlternatives<\/code> lets you send both from a rendered template.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from django.core.mail import EmailMultiAlternatives\nfrom django.template.loader import render_to_string\nfrom django.utils.html import strip_tags\n\ndef send_welcome(user):\n    html = render_to_string('emails\/welcome.html', {'user': user})\n    text = strip_tags(html)\n\n    email = EmailMultiAlternatives(\n        subject='Welcome to Your App',\n        body=text,\n        to=&#91;user.email],\n    )\n    email.attach_alternative(html, 'text\/html')\n    email.send(fail_silently=False)<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 6: Move Sending Off the Request Cycle<\/h3>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img decoding=\"async\" width=\"1024\" height=\"577\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_50_26-PM-1024x577.png\" alt=\"\tDjango Celery email flow showing the request returning immediately while a worker delivers the message through an SMTP relay\" class=\"wp-image-392\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_50_26-PM-1024x577.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_50_26-PM-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_50_26-PM-767x432.png 767w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_50_26-PM-1536x865.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-12-2026-01_50_26-PM.png 1671w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Celery returns the response immediately while a worker handles delivery separately.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Never make a user wait for an SMTP handshake during a web request. Push the send into Celery so the response returns immediately and delivery is retried independently.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># tasks.py\nfrom celery import shared_task\nfrom django.core.mail import EmailMultiAlternatives\n\n@shared_task(bind=True, max_retries=3, default_retry_delay=60)\ndef send_email_task(self, subject, text, html, to):\n    try:\n        email = EmailMultiAlternatives(subject, text, to=&#91;to])\n        email.attach_alternative(html, 'text\/html')\n        email.send(fail_silently=False)\n    except Exception as exc:\n        raise self.retry(exc=exc)<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># views.py\nsend_email_task.delay(subject, text, html, user.email)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/docs.celeryq.dev\/en\/stable\/\" target=\"_blank\" rel=\"noopener\">Celery documentation<\/a> covers worker configuration in depth. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-queue-architecture-explained\/\">transactional email queue architecture<\/a> covers the wider pattern, and <a href=\"https:\/\/photonconsole.com\/blog\/smtp-retry-logic-explained-for-transactional-email-systems\/\">SMTP retry logic<\/a> explains which failures are worth retrying.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quick Fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Celery Tasks Queue but Never Send<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Confirm a worker is actually running with <code>celery -A yourproject worker -l info<\/code><\/li>\n\n\n\n<li>Check the broker connection \u2014 a task queued to an unreachable broker fails silently<\/li>\n\n\n\n<li>Confirm the worker process has the same environment variables as the web process<\/li>\n\n\n\n<li>Restart workers after every deploy, as they hold old code and settings in memory<\/li>\n\n\n\n<li>Inspect failed tasks rather than assuming the SMTP connection is at fault<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Fixing the Password Reset Flow<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Django&#8217;s built-in password reset uses the same email configuration, so everything above applies. Three things specific to this flow cause failures.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The default template is plain and unbranded.<\/strong> Override <code>registration\/password_reset_email.html<\/code> to control what users receive. An unbranded reset email is more likely to be ignored or reported as suspicious.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The reset link uses the site domain.<\/strong> Django builds the URL from the Sites framework or the request. If <code>ALLOWED_HOSTS<\/code> or the Site record is wrong, users receive links pointing at <code>example.com<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Reset emails are time critical.<\/strong> Tokens expire, so a message delayed by an hour in a queue is useless by the time it arrives. Password resets should go through a priority path rather than sitting behind a bulk sending job.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Port and Encryption Reference<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>EMAIL_PORT<\/th><th>Setting<\/th><th>When to use<\/th><\/tr><\/thead><tbody><tr><td>587<\/td><td><code>EMAIL_USE_TLS = True<\/code><\/td><td>Recommended default<\/td><\/tr><tr><td>465<\/td><td><code>EMAIL_USE_SSL = True<\/code><\/td><td>When the provider requires implicit SSL<\/td><\/tr><tr><td>2525<\/td><td><code>EMAIL_USE_TLS = True<\/code><\/td><td>When the host blocks 587 and 465<\/td><\/tr><tr><td>25<\/td><td>Neither<\/td><td>Avoid \u2014 blocked by most hosting providers<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Configuring Django With PhotonConsole<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Moving Django onto a dedicated relay is a settings change plus DNS. No application code changes, and your templates and tasks stay exactly as they are.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">DNS Records<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>TXT    @                    v=spf1 include:relay.photonconsole.com ~all\nCNAME  photon._domainkey    dkim.photonconsole.com<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">DNS changes are not instant. SPF and DKIM records can take from a few minutes to 24-48 hours to propagate depending on your provider and TTL settings. Check you do not already publish a second SPF record \u2014 two separate TXT records starting with <code>v=spf1<\/code> break authentication entirely.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Settings<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'\nEMAIL_HOST = 'smtp.photonrelay.com'\nEMAIL_PORT = 587          # 465 for implicit SSL, 2525 if 587 is blocked\nEMAIL_HOST_USER = os.environ&#91;'PHOTON_USER']\nEMAIL_HOST_PASSWORD = os.environ&#91;'PHOTON_PASS']\nEMAIL_USE_TLS = True\nEMAIL_TIMEOUT = 10\nDEFAULT_FROM_EMAIL = 'Your App &lt;noreply@yourdomain.com&gt;'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Port 2525 exists specifically for hosting environments that block the standard SMTP ports, which resolves the AWS and DigitalOcean problem described earlier. Every PhotonConsole account includes 5,000 free emails per month, enough to validate the full setup \u2014 DNS records, Celery tasks and retry handling \u2014 before any spend. Details are on the <a href=\"https:\/\/www.photonconsole.com\/relay.php\">PhotonRelay page<\/a>, and <a href=\"https:\/\/www.photonconsole.com\/pricing.php\">pricing<\/a> has no monthly minimum.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are still comparing options, our analysis of <a href=\"https:\/\/photonconsole.com\/blog\/free-smtp-servers\/\">free SMTP servers<\/a> covers where each one breaks down under production load.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Environment-Specific Notes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Local Development<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use the console backend or a mail catcher such as Mailpit. Keep the SMTP backend configured in a separate settings module so the production code path is exercised before deploy day rather than discovered on it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Docker and Kubernetes<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Containers have no local mail transfer agent, so an external relay is mandatory. Inject credentials through secrets rather than baking them into the image, and confirm the Celery worker container receives the same environment variables as the web container.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Heroku and Platform Hosting<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Config vars must be set for every process type. A web dyno with correct mail settings and a worker dyno without them produces email that works synchronously and fails through Celery.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Serverless Deployments<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Persistent SMTP connections are impractical when the container is destroyed after each invocation. Keep function timeouts above <code>EMAIL_TIMEOUT<\/code> so a send is never cut off mid-handshake.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pro Tips for Reliable Django Email<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Never use <code>fail_silently=True<\/code> in production.<\/strong> It suppresses the exception that tells you what went wrong, turning a fixable error into a mystery.<\/li>\n\n\n\n<li><strong>Configure ADMINS and SERVER_EMAIL.<\/strong> Django emails unhandled exceptions to <code>ADMINS<\/code>, which is useless if the mail configuration is what broke.<\/li>\n\n\n\n<li><strong>Use a subdomain for application mail.<\/strong> Sending from <code>mail.yourdomain.com<\/code> isolates transactional reputation from your business email.<\/li>\n\n\n\n<li><strong>Verify DNS after any change.<\/strong> <a href=\"https:\/\/mxtoolbox.com\/\" target=\"_blank\" rel=\"noopener\">MXToolbox<\/a> confirms SPF, DKIM and blocklist status in a single lookup.<\/li>\n\n\n\n<li><strong>Score a real send before launch.<\/strong> <a href=\"https:\/\/www.mail-tester.com\/\" target=\"_blank\" rel=\"noopener\">Mail Tester<\/a> flags authentication problems before users encounter them.<\/li>\n\n\n\n<li><strong>Separate transactional and bulk queues.<\/strong> A newsletter batch that gets throttled should never delay a password reset.<\/li>\n\n\n\n<li><strong>Never log the full settings object.<\/strong> Log the exception and SMTP response, not the values containing your password.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Related Issues You May Hit Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-authentication-error\/\">SMTP authentication errors<\/a> when credentials are rejected despite being correct<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-not-working\/\">SMTP not working<\/a> across the ten most common failure modes<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/why-emails-go-to-spam-in-gmail\/\">Emails landing in Gmail spam<\/a> despite a successful send<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/emails-sent-but-not-delivered\/\">Emails sent but not delivered<\/a> when the SMTP response says success<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-configuration\/\">SMTP configuration<\/a> reference for host, port and encryption settings<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently Asked Questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Why does send_mail return 1 when no email arrives?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The return value counts messages the backend accepted, not messages delivered. With the console backend it counts messages printed to stdout. Check <code>EMAIL_BACKEND<\/code> first.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Should I use EMAIL_USE_TLS or EMAIL_USE_SSL?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>EMAIL_USE_TLS = True<\/code> with port 587 for most applications. Use <code>EMAIL_USE_SSL = True<\/code> with port 465 only when the provider requires implicit SSL. Setting both raises an error.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I send email asynchronously in Django?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use Celery with a broker such as Redis or RabbitMQ. Django has no built-in background task queue, so a synchronous <code>send()<\/code> call blocks the request until the SMTP conversation finishes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use Gmail SMTP with Django?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For local testing, yes. For production, no. Google enforces low sending caps, throttles automated traffic, and can lock the account, which takes your password reset emails down with it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I test email without sending real messages?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>locmem.EmailBackend<\/code> in tests and inspect <code>django.core.mail.outbox<\/code>. For manual checks, use the file-based backend or a mail catcher rather than sending to real addresses.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why do my Django emails go to spam?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Delivery and inbox placement are separate. A successful send only means the relay accepted the message. Missing SPF or DKIM records, or a from address that does not exist, are the usual causes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I still need SPF and DKIM when using a relay?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. The relay delivers the message, but authentication records prove your domain authorised it. Without them, delivery is far less reliable regardless of which relay you use.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Django&#8217;s email layer is dependable. Nearly every production failure traces to one of four things: the console backend left active, mismatched TLS and SSL settings, a default from address that does not exist, or a domain with no authentication records.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Fix those four and Django email works predictably. Add a timeout, reuse connections for batches, and move sending into Celery, and it stays reliable as traffic grows. What remains is infrastructure rather than framework: whether your host permits outbound SMTP, and whether receiving providers trust the domain you send from.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If messages are blocked by port restrictions, throttled by a mailbox provider, or filtered because your server has no sending reputation, no change to <code>settings.py<\/code> will fix it. A dedicated <a href=\"https:\/\/www.photonconsole.com\/relay.php\">transactional email solution<\/a> handles authentication, routing and reputation, using the same configuration shown above. Developers working across stacks may also want our guides to <a href=\"https:\/\/photonconsole.com\/blog\/sending-email-in-python-smtplib-vs-an-email-api-with-working-code\/\">sending email in Python with smtplib<\/a> and <a href=\"https:\/\/photonconsole.com\/blog\/how-to-send-emails-in-node-js-with-nodemailer-a-production-setup-guide\/\">sending email in Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Read More<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.photonconsole.com\/email-deliverability-checker.php\">Free Email Deliverability Checker<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/sending-email-in-python-smtplib-vs-an-email-api-with-working-code\/\">Sending Email in Python: smtplib vs an Email API<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-queue-architecture-explained\/\">Transactional Email Queue Architecture Explained<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/spf-dkim-dmarc-explained-simply\/\">SPF, DKIM and DMARC Explained Simply<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-response-codes-explained\/\">SMTP Response Codes Explained<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.photonconsole.com\/relay.php\">PhotonRelay: SMTP Relay Service<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.photonconsole.com\/pricing.php\">PhotonConsole Pricing<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Your Django app sends email perfectly in development. You deploy it, and the emails stop arriving. Password reset links never reach users, signup confirmations vanish, and send_mail() keeps returning 1 as though everything worked. Django is doing exactly what you configured it to do. The problem is that the settings which work on a laptop [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":390,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[312],"tags":[523,524,522,521,525],"class_list":["post-389","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-email-engineering-guide","tag-django-celery-email","tag-django-email-backend-production","tag-django-email-configuration","tag-django-send_mail-not-working","tag-django-smtp-settings"],"_links":{"self":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/389","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/comments?post=389"}],"version-history":[{"count":1,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/389\/revisions"}],"predecessor-version":[{"id":393,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/389\/revisions\/393"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media\/390"}],"wp:attachment":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media?parent=389"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/categories?post=389"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/tags?post=389"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}