tutorial

How to Monitor Tortoise MQTT with Vigilmon

Add uptime monitoring, heartbeat checks, and alerts to your Elixir MQTT application using the Tortoise library — covering broker connectivity, message flow, and IoT telemetry pipelines.

How to Monitor Tortoise MQTT with Vigilmon

Tortoise is an MQTT 3.1.1 client library for Elixir. It enables pub/sub messaging to brokers like EMQX, Mosquitto, and HiveMQ, with support for QoS 0/1/2, persistent sessions, last will messages, and TLS. It is the most common choice for IoT device telemetry ingestion in Nerves-based systems.

The challenge with MQTT-based applications is that failures are silent. The broker might be unreachable, a topic subscription might stall, or a background GenServer processing incoming messages might crash and never restart. None of these produce HTTP 500s. External monitoring is the only way to catch them before users or devices notice.

This tutorial adds production observability to a Tortoise application:

  • A health check HTTP endpoint that tests broker connectivity
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring for MQTT subscription handlers and telemetry pipelines
  • Slack alerts and a status page

Why Monitor a Tortoise Application?

MQTT applications fail in ways that don't generate obvious errors:

  • Broker unreachable: your Tortoise client reconnects in the background, silently dropping messages during the reconnection window
  • QoS 1/2 message queue overflow: unacknowledged messages pile up without alerting anything
  • Subscription handler crash: the handler process crashes, the supervisor restarts it, but messages delivered during the restart window are lost
  • TLS certificate expiry: the broker rejects the connection, Tortoise falls into a reconnect loop, and no messages flow

External monitoring catches all of these by verifying the application can actually connect to the broker and process messages end-to-end.


Key Metrics to Monitor

| Metric | Why It Matters | |--------|---------------| | Broker connectivity | Is the MQTT connection alive? | | Subscription handler liveness | Are incoming messages being processed? | | Message processing latency | Is the pipeline keeping up with device traffic? | | Reconnect rate | High reconnect frequency indicates network instability | | QoS message acknowledgment | Are QoS 1/2 messages being acknowledged? |


Step 1: Add a health check endpoint

Add a lightweight HTTP server alongside your MQTT application so Vigilmon has a URL to poll. If your app already uses Phoenix or Plug, add a route. For a standalone Tortoise app, use Bandit or Plug.Cowboy.

# mix.exs
defp deps do
  [
    {:tortoise, "~> 0.10"},
    {:bandit, "~> 1.0"},     # or {:plug_cowboy, "~> 2.0"}
    {:jason, "~> 1.4"},
    {:req, "~> 0.5"}
  ]
end

Create a health check plug that verifies the Tortoise connection is alive:

# lib/my_app/health_check.ex
defmodule MyApp.HealthCheck do
  import Plug.Conn

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = %{
      mqtt_connection: check_mqtt_connection(),
      subscription_handler: check_subscription_handler()
    }

    status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(%{status: status_label(status), checks: checks}))
    |> halt()
  end

  def call(conn, _opts), do: conn

  defp check_mqtt_connection do
    # Check if the Tortoise connection process is alive
    case Tortoise.Connection.ping(MyApp.MqttClient) do
      {:ok, _latency} -> :ok
      {:error, _reason} -> :error
    end
  rescue
    _ -> :error
  end

  defp check_subscription_handler do
    # Verify the handler GenServer is running
    case Process.whereis(MyApp.MessageHandler) do
      pid when is_pid(pid) ->
        if Process.alive?(pid), do: :ok, else: :error
      nil -> :error
    end
  end

  defp status_label(200), do: "ok"
  defp status_label(_), do: "degraded"
end

Wire it up with a minimal Plug router:

# lib/my_app/http_server.ex
defmodule MyApp.HttpServer do
  use Plug.Router

  plug MyApp.HealthCheck
  plug :match
  plug :dispatch

  get "/" do
    send_resp(conn, 200, "ok")
  end

  match _ do
    send_resp(conn, 404, "not found")
  end
end

Add the HTTP server to your supervision tree:

# lib/my_app/application.ex
def start(_type, _args) do
  children = [
    # Your Tortoise connection
    {Tortoise.Connection, client_id: MyApp.MqttClient,
      server: {Tortoise.Transport.Tcp, host: mqtt_host(), port: 1883},
      handler: {MyApp.MessageHandler, []}},

    # Your message handler GenServer
    MyApp.MessageHandler,

    # Health check HTTP server
    {Bandit, plug: MyApp.HttpServer, port: 4000}
  ]

  Supervisor.start_link(children, strategy: :one_for_one)
end

Test the health check:

mix run --no-halt &
curl http://localhost:4000/health
# {"status":"ok","checks":{"mqtt_connection":"ok","subscription_handler":"ok"}}

A 503 response body tells you exactly which subsystem failed — broker connectivity or handler liveness.


Step 2: Set up HTTP monitoring in Vigilmon

Point Vigilmon at your health endpoint:

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute (paid) or 5 minutes (free)
  5. Save

Vigilmon checks from multiple geographic regions. For IoT applications deployed in a specific region, you'll know whether broker connectivity is failing globally or locally.

Add a keyword check to verify your app is actually connected:

  1. In the monitor settings, add a Keyword Check
  2. Keyword: "mqtt_connection":"ok"
  3. Mode: must contain

This fails the monitor if the broker is unreachable even when the HTTP server returns 200 for other reasons.


Step 3: Heartbeat monitoring for message pipelines

The most critical failure mode for Tortoise applications is a silent pipeline stall. Messages stop flowing, but no error is raised. Heartbeat monitoring detects this.

Subscription handler heartbeat

Your Tortoise handler module receives messages via handle_message/3. Ping Vigilmon on each successful processing cycle:

# lib/my_app/message_handler.ex
defmodule MyApp.MessageHandler do
  use Tortoise.Handler

  require Logger

  @heartbeat_interval :timer.minutes(5)

  def init(_args) do
    schedule_heartbeat_check()
    {:ok, %{last_message_at: nil, message_count: 0}}
  end

  def handle_message(topic, payload, state) do
    case process_message(topic, payload) do
      :ok ->
        ping_heartbeat()
        {:ok, %{state | last_message_at: System.monotonic_time(), message_count: state.message_count + 1}}
      {:error, reason} ->
        Logger.error("Failed to process message on #{inspect(topic)}: #{inspect(reason)}")
        {:ok, state}
    end
  end

  # Also ping a "pipeline alive" heartbeat on a schedule even during quiet periods
  def handle_info(:heartbeat_check, state) do
    ping_pipeline_heartbeat()
    schedule_heartbeat_check()
    {:ok, state}
  end

  defp process_message(_topic, _payload), do: :ok  # your logic

  defp ping_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:message_heartbeat_url]
    if url, do: Task.start(fn -> Req.get(url, receive_timeout: 5_000) end)
  end

  defp ping_pipeline_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:pipeline_heartbeat_url]
    if url, do: Task.start(fn -> Req.get(url, receive_timeout: 5_000) end)
  end

  defp schedule_heartbeat_check do
    Process.send_after(self(), :heartbeat_check, @heartbeat_interval)
  end
end

Configure heartbeat URLs:

# config/runtime.exs
config :my_app, :vigilmon,
  message_heartbeat_url: System.get_env("VIGILMON_MESSAGE_HEARTBEAT_URL"),
  pipeline_heartbeat_url: System.get_env("VIGILMON_PIPELINE_HEARTBEAT_URL")

In Vigilmon, create a Heartbeat Monitor for each critical pipeline:

  1. Click New Monitor → Heartbeat
  2. Set expected interval: 5 minutes (match your @heartbeat_interval)
  3. Copy the ping URL
  4. Set it as VIGILMON_PIPELINE_HEARTBEAT_URL in your environment

If the Tortoise handler crashes and the supervisor gives up retrying, no heartbeat arrives and Vigilmon alerts you within the grace period.


Step 4: Alerts via Slack

In Vigilmon, go to Notifications → New Channel → Slack and paste your Slack incoming webhook URL.

To create a webhook:

  1. api.slack.com/appsCreate New App → From scratch
  2. Enable Incoming WebhooksAdd New Webhook
  3. Pick your IoT alerts channel and copy the URL

Enable the Slack channel on both your HTTP monitor and heartbeat monitors. When the MQTT pipeline stalls, Vigilmon sends:

🔴 DOWN: yourdomain.com/health
Keyword check failed: "mqtt_connection":"ok" not found
Detected from: EU-West, US-East

🔴 HEARTBEAT MISSED: IoT Pipeline
Expected ping every 5 minutes
Last ping: 23 minutes ago

For IoT systems ingesting device telemetry, a missed heartbeat alert means you stop data collection — which often has downstream effects on dashboards, billing, or device state management.


Step 5: Monitor broker connectivity separately

If you control the MQTT broker (self-hosted EMQX or Mosquitto), add a direct TCP monitor for the broker port:

  1. Click New Monitor → TCP
  2. Host: your broker hostname
  3. Port: 1883 (or 8883 for TLS)
  4. Interval: 1 minute

This distinguishes between two failure classes:

  • Broker down (TCP monitor fails): the broker itself is unreachable
  • Application down (HTTP monitor fails, TCP monitor passes): the Tortoise client or handler crashed

Clear separation means faster triage — you know immediately whether to restart the broker or your application.


Step 6: Status page and badge

Status page:

  1. Go to Status Pages → New Status Page in Vigilmon
  2. Add your HTTP monitor, heartbeat monitors, and TCP broker monitor
  3. Copy the public URL

Share the status page with your IoT device team or operations dashboard so everyone sees device telemetry health at a glance.

README badge:

![MQTT Pipeline Uptime](https://vigilmon.online/badge/your-monitor-slug)

What you've built

| What | How | |------|-----| | Health check endpoint | Plug that tests broker connectivity + handler liveness | | Broker connectivity check | Tortoise.Connection.ping/1 | | HTTP uptime monitoring | Vigilmon HTTP monitor → /health | | Keyword check | Verifies "mqtt_connection":"ok" in response | | Message pipeline heartbeat | Ping on each successful handle_message/3 cycle | | Scheduled pipeline heartbeat | Periodic ping even during quiet message periods | | Direct broker TCP check | Vigilmon TCP monitor on port 1883/8883 | | Slack alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

BEAM keeps your Tortoise processes alive. Vigilmon keeps external eyes on whether messages are actually flowing.


Next steps

  • Add per-topic message counters to the health response for richer monitoring context
  • Use Vigilmon's response time history to catch broker latency increases before they affect QoS guarantees
  • Add separate heartbeat monitors for each MQTT topic subscription that processes business-critical device events
  • If you use EMQX clustering, add a TCP monitor per broker node to detect partial cluster failures

Get started free at vigilmon.online.

Monitor your app with Vigilmon

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

Start free →