How to Send Transactional Email From Heroku (With Mailer To Go)
A Heroku dyno is a great place to run your app and a terrible place to run a mail server. The filesystem is ephemeral, dyno IPs are shared and rotate, and outbound port 25 is effectively unusable — so any mail you try to send straight from a dyno lands in spam, if it leaves at all. The fix is to hand sending to an email service. Mailer To Go is a Heroku add-on that provisions in seconds and wires itself into your app, so you send over an authenticated, reputation-managed connection on port 587.
This guide takes you from zero to a sent, inbox-landing email on Heroku, and covers the Heroku-specific gotchas (config vars, review apps, background jobs) that trip people up.
Step 1 — Provision the add-on
From the CLI:
heroku addons:create mailertogo --app your-app
Or add it from the Heroku Elements Marketplace. Either way, provisioning sets these config vars on your app automatically — no copy-pasting credentials:
heroku config --app your-app | grep MAILERTOGO
# MAILERTOGO_SMTP_HOST: smtp.mailertogo.com
# MAILERTOGO_SMTP_USER: ...
# MAILERTOGO_SMTP_PASSWORD: ...
# MAILERTOGO_API_KEY: ...
To declare it as a dependency so every deploy and review app gets it, add it to app.json:
{
"addons": ["mailertogo"]
}
Step 2 — Verify your sending domain
Add a sending subdomain — mtg.yourdomain.com — in the Mailer To Go dashboard and publish the SPF and DKIM records it shows you (plus a DMARC policy at your root). A subdomain keeps your app’s sending reputation separate from your root domain, and is exactly what makes mail from an ephemeral platform like Heroku land reliably. See Why Mailer To Go sends from mtg.yourdomain.com and the SPF/DKIM/DMARC setup guide.
Step 3 — Point your app at the config vars
Your app already has the credentials as environment variables — just reference them.
Rails:
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV["MAILERTOGO_SMTP_HOST"], # smtp.mailertogo.com
port: 587,
user_name: ENV["MAILERTOGO_SMTP_USER"],
password: ENV["MAILERTOGO_SMTP_PASSWORD"],
authentication: :plain,
enable_starttls: true
}
Node.js (Nodemailer):
const transport = require("nodemailer").createTransport({
host: process.env.MAILERTOGO_SMTP_HOST,
port: 587,
auth: { user: process.env.MAILERTOGO_SMTP_USER, pass: process.env.MAILERTOGO_SMTP_PASSWORD },
});
Prefer HTTP over SMTP, or on another language? Use the REST API instead — full examples in Send email with the Mailer To Go API & SMTP.
Step 4 — Deploy and send a test
git push heroku main
heroku run rails runner 'ActionMailer::Base.mail(from: "hello@mtg.yourdomain.com", to: "you@example.com", subject: "Hello from Heroku", body: "It works.").deliver_now' --app your-app
heroku logs --tail --app your-app # watch the send
Open the message in Gmail → Show original and confirm SPF, DKIM, and DMARC all say PASS.
Heroku-specific gotchas
- Never hardcode credentials. Setting a config var restarts your dynos and rotates cleanly; a hardcoded password survives a credential rotation and silently breaks. Always read from
ENV. - Send from a background job. The dyno filesystem is ephemeral and web requests have a 30-second timeout, so don’t send inline in a controller action. Enqueue to Sidekiq / Delayed Job / GoodJob and send from the worker — faster responses, and automatic retries on transient SMTP errors.
- Review apps and pipelines. With
mailertogoinapp.json, each review app can provision its own instance; if you’d rather not send real mail from ephemeral apps, override the mailer to:test(Rails) or a catch-all in those environments. - Port 25 is a dead end — you don’t need it. Heroku (like every major cloud) makes outbound port 25 unusable; Mailer To Go uses the submission port 587, which is open. If you ever see connections hang on port 25, that’s why.
- Scaling dynos is fine. SMTP is stateless per message, so running multiple web/worker dynos needs no special handling — every dyno reads the same config vars.
Why this works when sending from the dyno doesn’t
Deliverability is a reputation game played by the sending IP and domain. A Heroku dyno has neither a stable IP nor a warmed reputation, and can’t publish the authentication records inbox providers demand. Mailer To Go provides the managed IPs, the DKIM signing and key rotation, the SPF/DMARC alignment, and the bounce/complaint handling — so your dyno does what it’s good at (running your app) while your email rides on infrastructure built to get delivered.
Deploying through Addons.io or Build.io instead? The setup is identical — see Using Mailer To Go on Heroku, Addons.io & Build.io. Working with Heroku subdomains generally? See Heroku subdomains: an introductory guide.