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:
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval: 1 minute (paid) or 5 minutes (free)
- 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:
- In the monitor settings, add a Keyword Check
- Keyword:
"mqtt_connection":"ok" - 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:
- Click New Monitor → Heartbeat
- Set expected interval: 5 minutes (match your
@heartbeat_interval) - Copy the ping URL
- Set it as
VIGILMON_PIPELINE_HEARTBEAT_URLin 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:
- api.slack.com/apps → Create New App → From scratch
- Enable Incoming Webhooks → Add New Webhook
- 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:
- Click New Monitor → TCP
- Host: your broker hostname
- Port:
1883(or8883for TLS) - 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:
- Go to Status Pages → New Status Page in Vigilmon
- Add your HTTP monitor, heartbeat monitors, and TCP broker monitor
- 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:

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.