{"id":442,"date":"2026-09-26T01:30:59","date_gmt":"2026-09-26T07:00:59","guid":{"rendered":"https:\/\/photonconsole.com\/blog\/?p=442"},"modified":"2026-09-26T08:00:02","modified_gmt":"2026-09-26T13:30:02","slug":"ruby-on-rails-actionmailer-smtp-configuration-and-delivery-reliability","status":"publish","type":"post","link":"https:\/\/photonconsole.com\/blog\/ruby-on-rails-actionmailer-smtp-configuration-and-delivery-reliability\/","title":{"rendered":"Ruby on Rails ActionMailer: SMTP Configuration and Delivery Reliability"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">ActionMailer is one of the most pleasant parts of Rails. Generate a mailer, write a view, call <code>deliver_later<\/code>, and email works. Locally, at least.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In production the same code can fail without a sound. No exception, no log entry, no failed job. The controller returns a success response, the user waits for a password reset that never arrives, and nothing in your monitoring suggests anything happened.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That silence is a configuration default, not a bug. This guide covers the production setup: SMTP settings that actually connect, the flag that hides every delivery error, queuing with ActiveJob, mailer previews, and the errors Rails developers hit most often.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quick Answer: How Do You Configure ActionMailer SMTP?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Set the delivery method and SMTP settings in your production environment file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># config\/environments\/production.rb\nconfig.action_mailer.delivery_method = :smtp\nconfig.action_mailer.perform_deliveries = true\nconfig.action_mailer.raise_delivery_errors = true\nconfig.action_mailer.default_url_options = { host: \"yourapp.com\", protocol: \"https\" }\n\nconfig.action_mailer.smtp_settings = {\n  address:              \"smtp.photonrelay.com\",\n  port:                 587,\n  domain:               \"yourdomain.com\",\n  user_name:            ENV&#91;\"SMTP_USER\"],\n  password:             ENV&#91;\"SMTP_PASS\"],\n  authentication:       :plain,\n  enable_starttls_auto: true,\n  open_timeout:         5,\n  read_timeout:         5\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>class UserMailer &lt; ApplicationMailer\n  default from: \"noreply@yourdomain.com\"\n\n  def welcome(user)\n    @user = user\n    mail(to: @user.email, subject: \"Welcome to Your App\")\n  end\nend\n\nUserMailer.welcome(user).deliver_later<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Three lines in that configuration matter more than the rest, and the next sections explain why. Delivery itself depends on the server you authenticate against and whether your domain is authorised to send \u2014 an authenticated relay such as <a href=\"https:\/\/www.photonconsole.com\/\">PhotonConsole<\/a> covers that side.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How ActionMailer Delivers<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">ActionMailer builds the message and hands it to a delivery method. The <a href=\"https:\/\/guides.rubyonrails.org\/action_mailer_basics.html\" target=\"_blank\" rel=\"noopener\">Rails guides<\/a> document four, and only one sends real mail over the network.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Delivery method<\/th><th>What it does<\/th><th>Use in<\/th><\/tr><\/thead><tbody><tr><td><code>:smtp<\/code><\/td><td>Sends over SMTP to the configured server<\/td><td>Production<\/td><\/tr><tr><td><code>:sendmail<\/code><\/td><td>Pipes to a local sendmail binary<\/td><td>Rarely \u2014 usually unauthenticated<\/td><\/tr><tr><td><code>:file<\/code><\/td><td>Writes each message to <code>tmp\/mails<\/code><\/td><td>Local inspection<\/td><\/tr><tr><td><code>:test<\/code><\/td><td>Appends to <code>ActionMailer::Base.deliveries<\/code><\/td><td>Automated tests<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Underneath, ActionMailer uses the Mail gem to open the SMTP connection and speak the protocol. Rails does not deliver email itself \u2014 it hands a correctly formed message to whatever server you named.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Rails Email Fails Silently in Production<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_27_52-PM-1024x576.png\" alt=\"Diagram of four Rails ActionMailer settings that cause email to fail silently: raise_delivery_errors false, perform_deliveries false, async queue adapter and missing DNS records\" class=\"wp-image-444\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_27_52-PM-1024x576.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_27_52-PM-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_27_52-PM-768x432.png 768w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_27_52-PM-1536x864.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_27_52-PM.png 1672w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Four settings that let Rails report success while nothing is delivered.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">1. raise_delivery_errors Is Set to False<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rails&#8217; generated development config sets <code>raise_delivery_errors = false<\/code> so developers are not interrupted by mail failures while working offline. That block frequently gets copied into <code>production.rb<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With it off, every SMTP exception is swallowed. Bad credentials, an unreachable host, a rejected recipient \u2014 all of it disappears. Your application reports success because, as far as it knows, nothing went wrong.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. perform_deliveries Is Disabled<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Another development default. When <code>false<\/code>, ActionMailer builds the message and discards it. The mailer runs, the view renders, and no connection is ever opened.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. deliver_later Uses the Async Adapter<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rails ships with the <code>:async<\/code> queue adapter by default. It runs jobs on an in-process thread pool, which means queued email is held in memory. A deploy, a restart or a crash discards everything still waiting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a password reset that is a lost login. For a receipt it is a support ticket.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. default_url_options Is Missing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Mailer views have no request context, so URL helpers cannot infer a host. Without <code>default_url_options<\/code>, any mailer using a <code>_url<\/code> helper raises <code>Missing host to link to<\/code>. Under <code>deliver_later<\/code> that surfaces as a failed job rather than a request error, which is easy to miss.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. No Authentication Records on the Sending Domain<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Without SPF and DKIM, receiving servers cannot verify your application is authorised to send as your domain, in line with <a href=\"https:\/\/support.google.com\/mail\/answer\/81126\" target=\"_blank\" rel=\"noopener\">Google&#8217;s sender guidelines<\/a>. 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<p class=\"wp-block-paragraph\">Common Mistake<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Copying the ActionMailer block from <code>development.rb<\/code> into <code>production.rb<\/code>. That block exists to keep local development quiet, and it carries <code>raise_delivery_errors = false<\/code> with it. In production the result is an application that cannot tell you its email is broken \u2014 the failure mode most likely to go unnoticed for weeks. Set it to <code>true<\/code> in production, always.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quick Fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rails Sends No Email in Production<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Set <code>raise_delivery_errors = true<\/code> so the real exception surfaces<\/li>\n\n\n\n<li>Confirm <code>perform_deliveries = true<\/code><\/li>\n\n\n\n<li>Confirm <code>delivery_method = :smtp<\/code>, not <code>:test<\/code> or <code>:file<\/code><\/li>\n\n\n\n<li>Run a send from <code>rails console<\/code> on the server with <code>deliver_now<\/code> to see the error directly<\/li>\n\n\n\n<li>Try port 2525 if your host blocks 587 outbound<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step-by-Step: Production Setup<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Keep Credentials Out of the Repository<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use Rails encrypted credentials or environment variables. Never commit an SMTP password.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rails credentials:edit<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># config\/credentials.yml.enc\nsmtp:\n  user_name: your_project_api_user\n  password: your_secret_api_key<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>user_name: Rails.application.credentials.dig(:smtp, :user_name),\npassword:  Rails.application.credentials.dig(:smtp, :password),<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Always Set Timeouts<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Without <code>open_timeout<\/code> and <code>read_timeout<\/code>, a mail server that accepts a connection then stops responding can hold a worker indefinitely. Five seconds is a sensible default for both.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Verify From the Server, Not Your Laptop<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>rails console --environment=production<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>ActionMailer::Base.smtp_settings\n# confirm the values are what you expect\n\nUserMailer.welcome(User.first).deliver_now\n# deliver_now surfaces the exception immediately<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>deliver_now<\/code> for this test. <code>deliver_later<\/code> moves the failure into a background job where you may not see it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Use a Real Queue Adapter<\/h3>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_30_42-PM-1024x576.png\" alt=\"Comparison of Rails deliver_now blocking the request against deliver_later queueing to a durable worker that sends through an SMTP relay\" class=\"wp-image-445\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_30_42-PM-1024x576.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_30_42-PM-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_30_42-PM-767x431.png 767w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_30_42-PM-1536x864.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-Sep-26-2026-12_30_42-PM.png 1672w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">deliver_later only survives a restart when the queue adapter is durable.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Replace the in-memory async adapter with something durable before relying on <code>deliver_later<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># config\/environments\/production.rb\nconfig.active_job.queue_adapter = :sidekiq   # or :solid_queue, :good_job, :delayed_job<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>class UserMailer &lt; ApplicationMailer\n  default from: \"noreply@yourdomain.com\"\n\n  def password_reset(user)\n    @user = user\n    @reset_url = edit_password_reset_url(token: user.reset_token)\n    mail(to: @user.email, subject: \"Reset your password\")\n  end\nend<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Time-critical mail on its own queue\nUserMailer.password_reset(user).deliver_later(queue: :urgent)\n\n# Lower priority mail\nUserMailer.weekly_digest(user).deliver_later(queue: :low)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Separating queues matters more than it looks. A digest batch that backs up should never delay a password reset. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-queue-architecture-explained\/\">transactional email queue architecture<\/a> covers the 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\">deliver_later Queues but Never Delivers<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Confirm a worker process is actually running, not just the web server<\/li>\n\n\n\n<li>Check the queue adapter is not still <code>:async<\/code>, which loses jobs on restart<\/li>\n\n\n\n<li>Confirm the worker is listening on the queue name you passed<\/li>\n\n\n\n<li>Check the failed job backlog for the underlying exception<\/li>\n\n\n\n<li>Confirm the worker process has the same credentials as the web process<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Preview Before You Ship<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rails renders mailer previews in the browser, which catches layout and content problems without sending anything.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># test\/mailers\/previews\/user_mailer_preview.rb\nclass UserMailerPreview &lt; ActionMailer::Preview\n  def welcome\n    UserMailer.welcome(User.first)\n  end\n\n  def password_reset\n    UserMailer.password_reset(User.first)\n  end\nend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Visit <code>\/rails\/mailers<\/code> in development to see every preview. Previews render the real view with real data, so a missing interpolation or a broken URL helper shows up immediately.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Common Rails Delivery Errors<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Error<\/th><th>Usual cause<\/th><\/tr><\/thead><tbody><tr><td><code>Net::SMTPAuthenticationError<\/code><\/td><td>Wrong credentials, or whitespace copied into them<\/td><\/tr><tr><td><code>Net::OpenTimeout<\/code><\/td><td>Host unreachable or outbound SMTP blocked<\/td><\/tr><tr><td><code>EOFError<\/code><\/td><td>TLS mismatch \u2014 usually port 465 with STARTTLS settings<\/td><\/tr><tr><td><code>Missing host to link to<\/code><\/td><td><code>default_url_options<\/code> not set for that environment<\/td><\/tr><tr><td><code>Net::SMTPFatalError<\/code><\/td><td>Recipient rejected, or sender not authorised<\/td><\/tr><tr><td>No error at all<\/td><td><code>raise_delivery_errors<\/code> is false<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Note<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Port 465 needs different settings from 587. Use <code>enable_starttls_auto: true<\/code> on port 587, or <code>ssl: true<\/code> on port 465 \u2014 never both. Mixing them typically produces an <code>EOFError<\/code> rather than a message explaining the mismatch.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Port Reference<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Port<\/th><th>smtp_settings<\/th><th>When to use<\/th><\/tr><\/thead><tbody><tr><td>587<\/td><td><code>enable_starttls_auto: true<\/code><\/td><td>Recommended default<\/td><\/tr><tr><td>465<\/td><td><code>ssl: true<\/code><\/td><td>When the provider requires implicit SSL<\/td><\/tr><tr><td>2525<\/td><td><code>enable_starttls_auto: true<\/code><\/td><td>When the host blocks 587 and 465<\/td><\/tr><tr><td>25<\/td><td>n\/a<\/td><td>Avoid \u2014 blocked on nearly all hosts<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Configuring Rails With PhotonConsole<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two steps: authenticate the domain in DNS, then set <code>smtp_settings<\/code>. No mailer code changes.<\/p>\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<pre class=\"wp-block-code\"><code>config.action_mailer.smtp_settings = {\n  address:              \"smtp.photonrelay.com\",\n  port:                 587,          # 465 with ssl: true, 2525 if 587 is blocked\n  domain:               \"yourdomain.com\",\n  user_name:            Rails.application.credentials.dig(:smtp, :user_name),\n  password:             Rails.application.credentials.dig(:smtp, :password),\n  authentication:       :plain,\n  enable_starttls_auto: true,\n  open_timeout:         5,\n  read_timeout:         5\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>domain<\/code> value should be your sending domain, not the relay&#8217;s \u2014 it is what Rails announces in the SMTP HELO. DNS records can take up to 24-48 hours to propagate, so verify before assuming a configuration error. Every PhotonConsole account includes 5,000 free emails per month, enough to validate mailers, queues and previews 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 comparing options first, our analysis of <a href=\"https:\/\/photonconsole.com\/blog\/free-smtp-servers\/\">free SMTP servers<\/a> covers where each stops being practical.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Environment-Specific Notes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Development<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>delivery_method = :file<\/code> or a mail catcher such as Mailpit. Keep <code>raise_delivery_errors = false<\/code> here only, and never copy that line forward.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Staging<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use separate relay credentials so a staging mistake cannot affect production sending reputation. Consider intercepting all recipients so test mail cannot reach real users.<\/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 available to both web and worker dynos. A worker missing the SMTP credentials produces jobs that fail only when queued, which looks like a queue problem rather than a configuration one.<\/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 agent, so an external relay is required. Pass credentials as secrets, and remember the async adapter loses queued mail whenever a pod restarts.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AWS Hosting<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Outbound port 25 is restricted by default \u2014 AWS documents its <a href=\"https:\/\/repost.aws\/knowledge-center\/ec2-port-25-throttle\" target=\"_blank\" rel=\"noopener\">port 25 throttle removal process<\/a>. Use 587 or 2525 instead.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pro Tips<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Set <code>default from:<\/code> on ApplicationMailer.<\/strong> One place to change the sender rather than every mailer.<\/li>\n\n\n\n<li><strong>Use a monitored Reply-To.<\/strong> Users reply to transactional mail. A noreply address that bounces reads as untrustworthy.<\/li>\n\n\n\n<li><strong>Keep previews current.<\/strong> A preview for every mailer means layout regressions surface in review, not in a user&#8217;s inbox.<\/li>\n\n\n\n<li><strong>Never log the smtp_settings hash.<\/strong> It contains the password. Log the recipient and exception only.<\/li>\n\n\n\n<li><strong>Verify DNS after any change.<\/strong> <a href=\"https:\/\/mxtoolbox.com\/\" target=\"_blank\" rel=\"noopener\">MXToolbox<\/a> checks SPF, DKIM and blocklist status in one 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 hit them.<\/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<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-connection-timeout\/\">SMTP connection timeouts<\/a> when the connection hangs<\/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 server reports success<\/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 Rails send no email and raise no error?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>raise_delivery_errors<\/code> is almost certainly <code>false<\/code>, or <code>perform_deliveries<\/code> is <code>false<\/code>. Both are development defaults that often get copied into production.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Should I use deliver_now or deliver_later?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>deliver_later<\/code> for anything triggered by a web request, so the user does not wait on the SMTP handshake. <code>deliver_now<\/code> for console testing, where you want the exception immediately.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why do I get &#8220;Missing host to link to&#8221;?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Mailer views have no request context. Set <code>config.action_mailer.default_url_options<\/code> with a host for each environment.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is the async queue adapter safe for email?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not for anything important. It holds jobs in memory, so a deploy or restart discards queued mail. Use Sidekiq, Solid Queue, GoodJob or a similar durable adapter.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why do I get EOFError when connecting?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Usually a TLS mismatch. Port 465 needs <code>ssl: true<\/code>; port 587 needs <code>enable_starttls_auto: true<\/code>. Setting both, or the wrong one for the port, produces this error.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use Gmail SMTP with Rails?<\/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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I stop staging email reaching real users?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use an interceptor that rewrites every recipient to an internal address, registered only in the staging environment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">ActionMailer is reliable. Almost every production failure traces to a setting that exists to make local development quieter: delivery errors suppressed, deliveries disabled, or an in-memory queue adapter that silently drops work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Turn <code>raise_delivery_errors<\/code> on in production, confirm deliveries are enabled, set timeouts, move to a durable queue adapter, and give time-critical mail its own queue. That covers the framework side almost entirely.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What remains is infrastructure: whether your host permits outbound SMTP, and whether receiving providers trust the domain you send from. A dedicated <a href=\"https:\/\/www.photonconsole.com\/relay.php\">transactional email solution<\/a> handles authentication, routing and reputation using the 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<\/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\/transactional-email-queue-architecture-explained\/\">Transactional Email Queue Architecture Explained<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-retry-logic-explained-for-transactional-email-systems\/\">SMTP Retry Logic for Transactional Email Systems<\/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-configuration\/\">SMTP Configuration: Complete Setup Reference<\/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\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>ActionMailer is one of the most pleasant parts of Rails. Generate a mailer, write a view, call deliver_later, and email works. Locally, at least. In production the same code can fail without a sound. No exception, no log entry, no failed job. The controller returns a success response, the user waits for a password reset [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":443,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[31],"tags":[543,544],"class_list":["post-442","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-smpt-relay-service","tag-rails-sending-no-email-and-raising-no-error-fix-raise_delivery_errors","tag-the-async-queue-adapter-and-the-smtp-settings-that-quietly-break-delivery"],"_links":{"self":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/442","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=442"}],"version-history":[{"count":1,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/442\/revisions"}],"predecessor-version":[{"id":446,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/442\/revisions\/446"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media\/443"}],"wp:attachment":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media?parent=442"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/categories?post=442"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/tags?post=442"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}