tutorial

How to Monitor Bamboo Email with Vigilmon

Bamboo is the composable email library for Elixir and Phoenix with support for SMTP, Mailgun, SendGrid, and Mandrill adapters. Learn how to monitor email delivery pipelines, adapter connectivity, and background mailer jobs using Vigilmon.

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 like bamboo_smtp, bamboo_mailgun_adapter, or bamboo_sendgrid) in your mix.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

  1. Log in to vigilmon.online and click Add Monitor.
  2. Set Type to HTTP / HTTPS.
  3. Enter your health URL: https://myapp.example.com/health.
  4. Set Check interval to 1 minute.
  5. Set Expected HTTP status to 200.
  6. Under Response body, enable Contains keyword and enter "ok".
  7. 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:

  1. In Vigilmon, click Add MonitorCron Heartbeat.
  2. Set Expected ping interval to match your worker's processing cycle (e.g. 5 minutes for a queue-draining worker).
  3. 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:

  1. Click Add MonitorHTTP / HTTPS.
  2. Enter https://api.mailgun.net.
  3. Set Check interval to 2 minutes.
  4. Set Expected HTTP status to 200.

SendGrid:

  1. Click Add MonitorHTTP / HTTPS.
  2. Enter https://api.sendgrid.com.
  3. 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:

  1. Open the upstream API monitor from Step 4.
  2. Enable Monitor SSL certificate.
  3. Set Alert when certificate expires in less than 21 days.
  4. Click Save.

For self-hosted SMTP relays (Postfix, Exim, or services like Postal):

  1. Click Add MonitorTCP Port.
  2. Set Host to your SMTP relay hostname.
  3. Set Port to 587 (STARTTLS) or 465 (SMTPS).
  4. 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

  1. Go to Alert Channels in Vigilmon and add Slack, email, or PagerDuty.
  2. Set Consecutive failures before alert to 2 for HTTP monitors — brief API interruptions are common.
  3. Route email heartbeat failures to a high-priority channel (PagerDuty): missed delivery heartbeats mean transactional emails (password resets, purchase confirmations) are not going out.
  4. Route API upstream failures to Slack — useful context but typically the upstream provider resolves within minutes.
  5. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →