tutorial

How to Monitor Hammer with Vigilmon

Monitor your Elixir rate limiting layer with Vigilmon — detect Hammer backend failures, ETS table exhaustion, and Redis connectivity issues before they let traffic through unchecked or block legitimate users.

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

  1. Sign in at vigilmon.online and click Add Monitor.
  2. Choose HTTP monitor type.
  3. Enter https://api.example.com/health.
  4. Set check interval to 60 seconds.
  5. Expected status: 200.
  6. Under Response Checks, add: body contains "rate_limiter":"ok".
  7. 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:

  1. Open your monitor → Heartbeat tab.
  2. Copy the heartbeat URL → set as VIGILMON_HEARTBEAT_URL in your app environment.
  3. 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 Requests from 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.

Monitor your app with Vigilmon

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

Start free →