How to Monitor Hammer with Vigilmon
Hammer is Elixir's leading rate limiting library. It sits in your Phoenix plug pipeline or LiveView channel and enforces per-IP, per-user, or custom-key rate limits with pluggable backends: ETS for single-node deployments, Redis or Mnesia for distributed clusters.
When Hammer's backend becomes unavailable — Redis goes down, ETS gets corrupted, Mnesia loses quorum — Hammer typically fails open (lets all traffic through) or fails closed (blocks all traffic), depending on your configuration. Neither is what you want in production.
Vigilmon monitors your Phoenix API from the outside to catch rate limiter failures, backend outages, and abuse patterns before they impact your users or your infrastructure budget.
This tutorial covers:
- A health endpoint that validates Hammer's backend
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring for distributed Hammer backends
- Alerts for rate limiter failures and Redis connectivity issues
Step 1: Add a health endpoint that checks Hammer's backend
Hammer exposes Hammer.check_rate/3 — use it to verify the backend is operational:
# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
import Plug.Conn
require Logger
def init(opts), do: opts
def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
checks = %{
database: check_db(),
rate_limiter: check_hammer()
}
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_text(status), checks: checks}))
|> halt()
end
def call(conn, _opts), do: conn
defp check_db do
case MyApp.Repo.query("SELECT 1", []) do
{:ok, _} -> :ok
_ -> :error
end
end
defp check_hammer do
# Use a high limit so the health check itself never actually rate-limits
case Hammer.check_rate("health_check:internal", 60_000, 10_000) do
{:allow, _count} -> :ok
{:deny, _limit} -> :ok # deny means Hammer is working (10k checks in 1 min is normal)
{:error, reason} ->
Logger.error("Hammer backend check failed: #{inspect(reason)}")
:error
end
end
defp status_text(200), do: "ok"
defp status_text(_), do: "degraded"
end
Wire it into your Phoenix endpoint before the router:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router
Test it:
curl https://api.example.com/health
# {"status":"ok","checks":{"database":"ok","rate_limiter":"ok"}}
Step 2: Expose Hammer metrics (optional)
For more granular observability, add a metrics endpoint that reports current bucket state:
# lib/my_app_web/controllers/metrics_controller.ex
defmodule MyAppWeb.MetricsController do
use MyAppWeb, :controller
# Only accessible from internal network — add IP allowlist middleware
def index(conn, _params) do
# Inspect specific high-traffic buckets
buckets = [
{"api:global", 60_000, 10_000},
{"auth:login", 60_000, 5}
]
stats =
Enum.map(buckets, fn {key, scale, limit} ->
case Hammer.check_rate(key, scale, limit) do
{:allow, count} -> %{bucket: key, count: count, limit: limit, status: "ok"}
{:deny, _} -> %{bucket: key, count: limit, limit: limit, status: "rate_limited"}
{:error, _} -> %{bucket: key, count: nil, limit: limit, status: "error"}
end
end)
json(conn, %{buckets: stats})
end
end
Step 3: Set up Vigilmon HTTP monitoring
- Sign in at vigilmon.online and click Add Monitor.
- Choose HTTP monitor type.
- Enter
https://api.example.com/health. - Set check interval to 60 seconds.
- Expected status:
200. - Under Response Checks, add: body contains
"rate_limiter":"ok". - Click Save.
The body assertion ensures Vigilmon alerts when Hammer's backend is degraded even if the overall HTTP status is 200.
Step 4: Monitor Redis backend connectivity
If you use Hammer with a Redis backend (hammer_backend_redis), add a dedicated Redis health check:
defp check_redis do
case Redix.command(:redix, ["PING"]) do
{:ok, "PONG"} -> :ok
{:error, reason} ->
Logger.error("Redis health check failed: #{inspect(reason)}")
:error
end
end
Add :redis to your checks map in the health plug. Vigilmon will then alert on Redis connectivity loss, which prevents Hammer from enforcing limits across nodes in a distributed Phoenix cluster.
Step 5: Add a heartbeat for distributed Hammer (Mnesia/Redis)
For distributed deployments, add a heartbeat GenServer that validates cross-node rate limiting is working:
# lib/my_app/hammer_heartbeat.ex
defmodule MyApp.HammerHeartbeat do
use GenServer
require Logger
@interval_ms 60_000
@test_key "heartbeat:hammer_health"
@scale_ms 120_000
@limit 100
def start_link(_opts), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
schedule_check()
{:ok, state}
end
@impl true
def handle_info(:check, state) do
case Hammer.check_rate(@test_key, @scale_ms, @limit) do
{:error, reason} ->
Logger.error("Hammer distributed check failed: #{inspect(reason)}")
_ ->
ping_vigilmon()
end
schedule_check()
{:noreply, state}
end
defp ping_vigilmon do
url = System.get_env("VIGILMON_HEARTBEAT_URL")
if url, do: :httpc.request(:get, {String.to_charlist(url), []}, [], [])
end
defp schedule_check, do: Process.send_after(self(), :check, @interval_ms)
end
In Vigilmon:
- Open your monitor → Heartbeat tab.
- Copy the heartbeat URL → set as
VIGILMON_HEARTBEAT_URLin your app environment. - Grace period: 3 minutes (three missed 1-minute checks = alert).
Step 6: Write integration tests for your rate limiter
# test/my_app_web/rate_limit_test.exs
defmodule MyAppWeb.RateLimitTest do
use MyAppWeb.ConnCase
@tag :integration
test "allows requests within limit" do
for _ <- 1..5 do
conn = build_conn() |> get("/api/public-endpoint")
assert conn.status != 429
end
end
@tag :integration
test "blocks requests over limit" do
# Hit the endpoint enough to trigger rate limiting
conns = for _ <- 1..101, do: build_conn() |> get("/api/public-endpoint")
assert Enum.any?(conns, fn conn -> conn.status == 429 end)
end
test "health endpoint reports hammer ok" do
conn = build_conn() |> get("/health")
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
assert body["checks"]["rate_limiter"] == "ok"
end
end
Step 7: Configure alerts
In Vigilmon go to Alerts and add notification channels:
- Slack (
#api-health) — real-time ops visibility - Email — backend on-call
- PagerDuty — for APIs where rate limiting is a security control
Recommended alert policies:
| Condition | Severity | Action |
|---|---|---|
| HTTP non-200 | Critical | Page on-call |
| rate_limiter check = error | Critical | Alert — Hammer backend down, no enforcement |
| Redis check = error | High | Alert — distributed limits broken |
| Heartbeat missed ≥ 3× | Critical | Distributed Hammer health loop dead |
| Response latency > 1 s | Warning | ETS or Redis contention |
Why Monitor Hammer?
Rate limiting is a silent dependency. When Hammer's backend fails:
- Fail-open: all requests pass through — an abuse attack that was previously blocked now drains your infrastructure and budget
- Fail-closed: all requests are blocked — legitimate users get
429 Too Many Requestsfrom a service that should be responding normally - Stale ETS: on a node restart, in-memory rate limit counters reset, briefly allowing bursts above your intended limits
Vigilmon's health checks and heartbeat monitoring catch all three scenarios. The body assertion ("rate_limiter":"ok") is the key — it validates Hammer's backend, not just the HTTP server.
Key Metrics to Watch
| Metric | Threshold | Meaning |
|---|---|---|
| HTTP status | Must be 200 | API reachable |
| rate_limiter check | Must be ok | Hammer backend functional |
| redis check | Must be ok | Distributed backend reachable |
| Heartbeat interval | < 3 missed | Health loop alive |
| Response latency | < 500 ms | No ETS or Redis bottleneck |
Conclusion
Hammer makes per-user and per-IP rate limiting idiomatic in Elixir — and Vigilmon makes Hammer's health a first-class monitoring concern. A health endpoint that exercises the Hammer backend, a body assertion in Vigilmon, and a heartbeat for distributed deployments gives you confidence that your rate limiter is actually enforcing limits, not silently failing open or closed.
Get started free at vigilmon.online.