Bamboo is the email library that Elixir and Phoenix applications reach for when they need composable, testable email delivery — supporting SMTP, Mailgun, SendGrid, Mandrill, and custom adapters through a unified API. Transactional emails are silent infrastructure: a broken Bamboo adapter delivers no error to the end user, a crashed mailer worker queues jobs indefinitely, and an expired Mailgun API key causes silent drops that only surface when a user says they never received their password reset. Vigilmon gives you health monitoring, heartbeat checks, and alerting so Bamboo adapter failures and delivery pipeline breakdowns are caught before they affect your users.
What You'll Set Up
- HTTP health endpoint that validates your Bamboo adapter configuration and connectivity
- Cron heartbeat for background email delivery workers (Oban, Broadway, or GenServer-based)
- Latency monitoring for the email delivery API endpoint your adapter calls
- SSL certificate monitoring for SMTP and API adapter endpoints
Prerequisites
- Elixir 1.14+ with
bamboo(and a delivery adapter likebamboo_smtp,bamboo_mailgun_adapter, orbamboo_sendgrid) in yourmix.exs - A Phoenix or Plug application with at least one Bamboo mailer in production use
- A free Vigilmon account
Step 1: Add a Health Endpoint That Tests Adapter Connectivity
Rather than a trivial ping, exercise your Bamboo adapter configuration in the health check. For API-based adapters (Mailgun, SendGrid), verify the API key is set and the endpoint is reachable. For SMTP, confirm the connection can be established:
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def index(conn, _params) do
checks = %{
bamboo_adapter: check_bamboo_adapter(),
mailer_worker: check_mailer_worker()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
conn
|> put_status(status)
|> json(%{
status: if(status == 200, do: "ok", else: "degraded"),
checks: checks
})
end
defp check_bamboo_adapter do
# Verify adapter config keys are present (catches misconfigured deploys)
config = Application.get_env(:my_app, MyApp.Mailer, [])
adapter = Keyword.get(config, :adapter)
cond do
is_nil(adapter) ->
:error
adapter == Bamboo.MailgunAdapter ->
api_key = Keyword.get(config, :api_key) || System.get_env("MAILGUN_API_KEY")
if api_key && String.length(api_key) > 10, do: :ok, else: :error
adapter == Bamboo.SendGridAdapter ->
api_key = Keyword.get(config, :api_key) || System.get_env("SENDGRID_API_KEY")
if api_key && String.length(api_key) > 10, do: :ok, else: :error
adapter == Bamboo.SMTPAdapter ->
host = Keyword.get(config, :server) || System.get_env("SMTP_HOST")
if host && String.length(host) > 0, do: :ok, else: :error
true ->
:ok # Custom or test adapter
end
end
defp check_mailer_worker do
case Process.whereis(MyApp.MailerWorker) do
nil -> :error
pid -> if Process.alive?(pid), do: :ok, else: :error
end
end
end
A typical Bamboo mailer and configuration:
# lib/my_app/mailer.ex
defmodule MyApp.Mailer do
use Bamboo.Mailer, otp_app: :my_app
end
# config/runtime.exs
config :my_app, MyApp.Mailer,
adapter: Bamboo.MailgunAdapter,
api_key: System.get_env("MAILGUN_API_KEY"),
domain: System.get_env("MAILGUN_DOMAIN")
Wire the health route and verify:
mix phx.server
curl http://localhost:4000/health
# {"status":"ok","checks":{"bamboo_adapter":"ok","mailer_worker":"ok"}}
Step 2: Add the Monitor in Vigilmon
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your health URL:
https://myapp.example.com/health. - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Under Response body, enable Contains keyword and enter
"ok". - Click Save.
The keyword check catches the split case where your app returns 200 but the Bamboo adapter config reports "degraded" — this happens after deploys where secrets are missing from the environment.
Step 3: Heartbeat for Background Email Delivery Workers
Bamboo supports a deliver_later/1 function for async delivery. When used with a custom delivery queue (Oban, Broadway, or a GenServer worker), silent crashes mean emails queue forever without an alert.
Add a Vigilmon cron heartbeat:
- In Vigilmon, click Add Monitor → Cron Heartbeat.
- Set Expected ping interval to match your worker's processing cycle (e.g.
5minutes for a queue-draining worker). - Copy the heartbeat URL (e.g.
https://vigilmon.online/heartbeat/abc123).
For an Oban-based email worker:
# lib/my_app/workers/email_delivery_worker.ex
defmodule MyApp.Workers.EmailDeliveryWorker do
use Oban.Worker, queue: :emails, max_attempts: 3
require Logger
@impl Oban.Worker
def perform(%Oban.Job{args: %{"type" => type, "to" => to, "data" => data}}) do
email = build_email(type, to, data)
case MyApp.Mailer.deliver_now(email) do
{:ok, _email} ->
ping_heartbeat()
:ok
{:error, reason} ->
Logger.error("Bamboo delivery failed for #{type} to #{to}: #{inspect(reason)}")
{:error, reason}
end
end
defp build_email("welcome", to, data), do: MyApp.Emails.welcome(to, data)
defp build_email("password_reset", to, data), do: MyApp.Emails.password_reset(to, data)
defp build_email(type, to, _data) do
Logger.warning("Unknown email type: #{type} for #{to}")
MyApp.Emails.generic(to, type)
end
defp ping_heartbeat do
url = System.get_env("VIGILMON_EMAIL_HEARTBEAT_URL")
if url, do: :httpc.request(:get, {String.to_charlist(url), []}, [{:timeout, 5000}], [])
end
end
For a GenServer-based delivery worker:
# lib/my_app/mailer_worker.ex
defmodule MyApp.MailerWorker do
use GenServer
require Logger
@drain_interval 60_000
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def enqueue(email), do: GenServer.cast(__MODULE__, {:enqueue, email})
@impl true
def init(state), do: {:ok, Map.put(state, :queue, :queue.new()), {:continue, :schedule_drain}}
@impl true
def handle_continue(:schedule_drain, state) do
Process.send_after(self(), :drain, @drain_interval)
{:noreply, state}
end
@impl true
def handle_cast({:enqueue, email}, %{queue: q} = state) do
{:noreply, %{state | queue: :queue.in(email, q)}}
end
@impl true
def handle_info(:drain, %{queue: q} = state) do
{delivered, failed, new_queue} = drain_queue(q)
Logger.info("Email drain: #{delivered} delivered, #{failed} failed")
if delivered > 0, do: ping_heartbeat()
Process.send_after(self(), :drain, @drain_interval)
{:noreply, %{state | queue: new_queue}}
end
defp drain_queue(q) do
drain_queue(q, 0, 0, :queue.new())
end
defp drain_queue(q, delivered, failed, failed_q) do
case :queue.out(q) do
{{:value, email}, rest} ->
case MyApp.Mailer.deliver_now(email) do
{:ok, _} -> drain_queue(rest, delivered + 1, failed, failed_q)
{:error, _} -> drain_queue(rest, delivered, failed + 1, :queue.in(email, failed_q))
end
{:empty, _} ->
{delivered, failed, failed_q}
end
end
defp ping_heartbeat do
url = System.get_env("VIGILMON_EMAIL_HEARTBEAT_URL")
if url, do: :httpc.request(:get, {String.to_charlist(url), []}, [{:timeout, 5000}], [])
end
end
If the worker crashes and the supervisor exhausts its restart budget, Vigilmon alerts after the expected window expires — no more silent email delivery stalls.
Step 4: Monitor the Email API Endpoint Directly
For Mailgun, SendGrid, or Mandrill adapters, add a dedicated Vigilmon HTTP monitor for the delivery API:
Mailgun:
- Click Add Monitor → HTTP / HTTPS.
- Enter
https://api.mailgun.net. - Set Check interval to
2 minutes. - Set Expected HTTP status to
200.
SendGrid:
- Click Add Monitor → HTTP / HTTPS.
- Enter
https://api.sendgrid.com. - Set Check interval to
2 minutes.
When the email API goes down, Bamboo's deliver_now/1 calls will return {:error, _} — but your Oban or GenServer worker may not surface that as an alert unless you've instrumented it. The upstream API monitor gives you independent confirmation and lets you distinguish between your adapter configuration being broken versus the upstream provider being down.
Step 5: Monitor SSL Certificates for SMTP and API Endpoints
For SMTP-based Bamboo adapters, expired TLS certificates on the SMTP relay cause connection failures that surface as cryptic SSL errors rather than delivery bounces:
- Open the upstream API monitor from Step 4.
- Enable Monitor SSL certificate.
- Set Alert when certificate expires in less than
21 days. - Click Save.
For self-hosted SMTP relays (Postfix, Exim, or services like Postal):
- Click Add Monitor → TCP Port.
- Set Host to your SMTP relay hostname.
- Set Port to
587(STARTTLS) or465(SMTPS). - Set Check interval to
2 minutes.
SMTP relay connectivity failures cause deliver_now/1 to raise rather than return {:error, _}, which can crash your delivery worker unless you've wrapped calls in try/rescue.
Step 6: Configure Alert Channels
- Go to Alert Channels in Vigilmon and add Slack, email, or PagerDuty.
- Set Consecutive failures before alert to
2for HTTP monitors — brief API interruptions are common. - Route email heartbeat failures to a high-priority channel (PagerDuty): missed delivery heartbeats mean transactional emails (password resets, purchase confirmations) are not going out.
- Route API upstream failures to Slack — useful context but typically the upstream provider resolves within minutes.
- Use Maintenance windows when switching Bamboo adapters (e.g. migrating from SMTP to Mailgun):
curl -X POST https://vigilmon.online/api/maintenance \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"monitor_id": "abc123", "duration_minutes": 20}'
Summary
| Monitor | Target | What It Catches |
|---|---|---|
| HTTP health check | /health on your app | Missing adapter config, crashed mailer worker |
| Response keyword | "ok" in body | Adapter misconfigured despite 200 response |
| Cron heartbeat | Heartbeat URL | Silent delivery worker failure, queued emails not draining |
| Upstream HTTP monitor | Mailgun / SendGrid / SMTP API | Upstream provider downtime independent of your app |
| SSL certificate | Email API / SMTP relay | TLS expiry causing connection failures in deliver calls |
| TCP port monitor | SMTP relay port 587/465 | SMTP relay unreachable, firewall blocking delivery |
Bamboo makes email delivery composable and testable in Elixir — Vigilmon makes sure the delivery pipeline stays healthy in production. With an adapter-exercising health endpoint, heartbeat monitoring for async delivery workers, and upstream API monitoring, you'll know about email failures before your users start asking why they never received their confirmation emails.