How to Monitor Mint with Vigilmon
Mint is Elixir's low-level HTTP client, designed for building higher-level HTTP clients rather than calling APIs directly. It gives you full control over HTTP/1 and HTTP/2 connections — but that control comes with responsibility. Unlike higher-level clients, Mint connections are process-owned and not supervised by default, which means connection failures can leave your application quietly degraded.
This tutorial sets up layered monitoring for Mint-backed services:
- A health endpoint that verifies Mint can establish connections
- HTTP uptime monitoring with Vigilmon
- Connection pool health via heartbeats
- HTTP/2 multiplexing health checks
- Alerts when connection establishment fails
Why Monitor Mint?
| Signal | What it catches | |---|---| | Connection establishment | TCP/TLS handshake failures to upstream hosts | | HTTP/2 settings negotiation | Protocol negotiation failures during ALPN | | Response streaming | Incomplete response bodies from slow upstreams | | Connection pool exhaustion | All connections busy, new requests queuing | | TLS certificate issues | Expired or mismatched upstream certificates |
Mint is often used as the backend for Finch and other pooled clients. Failures at this layer affect every service that depends on those pools.
Step 1: Create a Mint Connection Health Check
Mint connections are process-owned, so a health check needs to open a real connection and make a request:
# lib/my_app/health/mint_health.ex
defmodule MyApp.Health.MintHealth do
@moduledoc """
Tests Mint HTTP/1 and HTTP/2 connection establishment to configured upstream hosts.
"""
@timeout 5_000
def check_upstream(host, port \\ 443, scheme \\ :https) do
with {:ok, conn} <- Mint.HTTP.connect(scheme, host, port, []),
{:ok, conn, request_ref} <- Mint.HTTP.request(conn, "GET", "/", [], nil),
{:ok, _conn, response} <- receive_response(conn, request_ref) do
status = get_status(response)
{:ok, %{host: host, status: status, http_version: conn.http_version}}
else
{:error, reason} ->
{:error, %{host: host, reason: inspect(reason)}}
{:error, conn, reason, _responses} ->
Mint.HTTP.close(conn)
{:error, %{host: host, reason: inspect(reason)}}
end
end
defp receive_response(conn, request_ref) do
receive do
message ->
case Mint.HTTP.stream(conn, message) do
{:ok, conn, responses} ->
if done?(responses, request_ref) do
{:ok, conn, responses}
else
receive_response(conn, request_ref)
end
{:error, conn, reason, _responses} ->
{:error, conn, reason, []}
end
after
@timeout -> {:error, conn, :timeout, []}
end
end
defp done?(responses, ref) do
Enum.any?(responses, fn
{:done, ^ref} -> true
_ -> false
end)
end
defp get_status(responses) do
case Enum.find(responses, &match?({:status, _, _}, &1)) do
{:status, _ref, status} -> status
nil -> nil
end
end
end
Add a controller endpoint:
# lib/my_app_web/controllers/health_controller.ex
def mint(conn, _params) do
upstreams = Application.get_env(:my_app, :monitored_upstreams, [
{"api.stripe.com", 443, :https},
{"api.sendgrid.com", 443, :https}
])
results = Enum.map(upstreams, fn {host, port, scheme} ->
MyApp.Health.MintHealth.check_upstream(host, port, scheme)
end)
failed = Enum.filter(results, &match?({:error, _}, &1))
if Enum.empty?(failed) do
json(conn, %{status: "ok", checked: length(results)})
else
conn
|> put_status(503)
|> json(%{status: "degraded", failures: Enum.map(failed, fn {:error, r} -> r end)})
end
end
Step 2: Monitor HTTP/2 Multiplexing Health
HTTP/2 allows multiple requests over a single connection. Add a check that verifies H2 negotiation succeeds:
# lib/my_app/health/mint_http2_health.ex
defmodule MyApp.Health.MintHTTP2Health do
def check_h2(host, port \\ 443) do
opts = [transport_opts: [alpn_advertised_protocols: ["h2", "http/1.1"]]]
case Mint.HTTP.connect(:https, host, port, opts) do
{:ok, conn} ->
version = conn.http_version
Mint.HTTP.close(conn)
{:ok, %{host: host, negotiated: version, http2: version == :"HTTP/2"}}
{:error, reason} ->
{:error, %{host: host, reason: inspect(reason)}}
end
end
end
This catches scenarios where a host drops HTTP/2 support (forcing all requests to HTTP/1.1 and reducing throughput significantly).
Step 3: Add Vigilmon Uptime Monitoring
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your health endpoint:
https://yourapp.com/health/mint. - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Under Advanced, add a Response body contains check:
"status":"ok". - Click Save.
Vigilmon now verifies your Mint connection pool can reach its upstreams every minute.
Step 4: Add a Heartbeat for Finch-based Connection Pools
Mint underpins Finch, which manages pooled connections. Monitor pool health with a heartbeat:
# lib/my_app/workers/finch_health_worker.ex
defmodule MyApp.Workers.FinchHealthWorker do
use GenServer
@interval :timer.minutes(1)
@vigilmon_heartbeat_url "https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"
@upstream_url "https://api.example.com/health"
def start_link(_), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def init(_) do
schedule()
{:ok, nil}
end
def handle_info(:check, state) do
case check_pool_health() do
:ok -> ping_vigilmon()
{:error, reason} ->
require Logger
Logger.warning("Mint/Finch pool health check failed: #{inspect(reason)}")
end
schedule()
{:noreply, state}
end
defp check_pool_health do
request = Finch.build(:get, @upstream_url)
case Finch.request(request, MyApp.Finch, receive_timeout: 5_000) do
{:ok, %Finch.Response{status: status}} when status in 200..299 -> :ok
{:ok, %Finch.Response{status: status}} -> {:error, {:unexpected_status, status}}
{:error, reason} -> {:error, reason}
end
end
defp ping_vigilmon do
request = Finch.build(:get, @vigilmon_heartbeat_url)
Finch.request(request, MyApp.Finch)
end
defp schedule, do: Process.send_after(self(), :check, @interval)
end
Add it to your supervision tree in application.ex:
children = [
{Finch, name: MyApp.Finch},
MyApp.Workers.FinchHealthWorker,
# ...
]
To create the heartbeat in Vigilmon:
- Click Add Monitor → Cron / Heartbeat.
- Set the expected ping interval to
1 minute. - Copy the generated heartbeat URL into
@vigilmon_heartbeat_url.
Step 5: Set Up Alerts
- Go to Alert Channels → Add Channel.
- Choose Slack, Email, or PagerDuty.
- Set alert thresholds: 2 consecutive failures before alerting.
- Assign the channel to your Mint HTTP monitor and heartbeat.
For services where HTTP/2 is critical for throughput (high-concurrency APIs), add a separate monitor for the H2 health endpoint and set a tighter alert threshold.
Key Metrics to Watch
| Metric | Vigilmon feature | What to alert on |
|---|---|---|
| Upstream connectivity | HTTP monitor on /health/mint | Any 5xx or timeout |
| Connection pool health | Heartbeat monitor | Missing ping for > 2 min |
| Response time | Response time chart | P95 > 2s over 5 minutes |
| SSL certificate | Cert expiry monitor | Expires in < 14 days |
| HTTP/2 negotiation | HTTP monitor on H2 endpoint | Non-200 response |
Conclusion
Mint's low-level design gives Elixir developers precise control over HTTP connections, but that control requires explicit monitoring. A health endpoint that opens real Mint connections, a Finch pool heartbeat, and Vigilmon watching both gives you early warning before connection failures cascade into application errors.
Get started free at vigilmon.online.