tutorial

How to Monitor Cachex with Vigilmon

Add health checks, uptime monitoring, and alerts to Elixir apps using Cachex — detect cache misses, memory pressure, and TTL drift before they degrade application performance.

How to Monitor Cachex with Vigilmon

Cachex is the most feature-rich caching library in the Elixir ecosystem. It supports TTL, lazy loading, hooks, limit-based eviction, transactions, and replication — all backed by ETS for sub-microsecond lookups.

But a cache that nobody watches is a liability. Unbounded growth causes memory pressure. High miss rates defeat the purpose of caching. Silent eviction policy mismatches mean your cache is doing nothing useful. A health check and external monitoring turns Cachex from a black box into an observable component.

This tutorial sets up production observability for Cachex with Vigilmon:

  • A health endpoint that reports cache hit rate, memory usage, and size
  • Uptime monitoring so you know when your app is unreachable
  • Heartbeat monitoring for scheduled cache warm-up jobs
  • Slack alerts when cache health degrades

Step 1: Expose Cachex stats in a health endpoint

Cachex's Cachex.stats/1 returns hit count, miss count, and eviction count. Use these to compute hit rate and expose cache health alongside your standard checks.

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = run_checks()
    status = if all_ok?(checks), 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 run_checks do
    %{
      database: check_database(),
      cache: check_cache(),
      memory: check_memory()
    }
  end

  defp check_cache do
    cache_name = :my_app_cache

    with {:ok, stats} <- Cachex.stats(cache_name),
         {:ok, size} <- Cachex.size(cache_name) do

      hits = get_in(stats, [:hits]) || 0
      misses = get_in(stats, [:misses]) || 0
      total = hits + misses

      hit_rate = if total > 0, do: hits / total * 100, else: 100.0

      cond do
        size > 500_000 -> :oversized
        hit_rate < 20 and total > 1000 -> :degraded
        true -> :ok
      end
    else
      _ -> :error
    end
  end

  defp check_database do
    case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
      {:ok, _} -> :ok
      {:error, _} -> :error
    end
  end

  defp check_memory do
    case :memsup.get_system_memory_data() do
      [] -> :ok
      data ->
        total = Keyword.get(data, :total_memory, 1)
        free = Keyword.get(data, :free_memory, total)
        if (total - free) / total * 100 < 90, do: :ok, else: :error
    end
  end

  defp all_ok?(checks) do
    Enum.all?(checks, fn {_, v} -> v not in [:error] end)
  end

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

Add a dedicated cache stats endpoint for richer monitoring:

# lib/my_app_web/controllers/cache_stats_controller.ex
defmodule MyAppWeb.CacheStatsController do
  use MyAppWeb, :controller

  @cache :my_app_cache

  def index(conn, _params) do
    stats = build_stats()
    json(conn, stats)
  end

  defp build_stats do
    with {:ok, raw_stats} <- Cachex.stats(@cache),
         {:ok, size} <- Cachex.size(@cache) do

      hits = get_in(raw_stats, [:hits]) || 0
      misses = get_in(raw_stats, [:misses]) || 0
      evictions = get_in(raw_stats, [:evictions]) || 0
      total = hits + misses
      hit_rate = if total > 0, Float.round(hits / total * 100, 2), else: nil

      %{
        size: size,
        hits: hits,
        misses: misses,
        evictions: evictions,
        hit_rate_percent: hit_rate,
        healthy: hit_rate == nil or hit_rate >= 20
      }
    else
      _ -> %{error: "cache unavailable", healthy: false}
    end
  end
end

Wire them up:

# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck

# lib/my_app_web/router.ex
get "/cache/stats", CacheStatsController, :index

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

For the cache stats endpoint, add a keyword monitor:

  1. Click New Monitor → HTTP
  2. Enter https://yourdomain.com/cache/stats
  3. Under Keyword Check, add "healthy":true
  4. Vigilmon alerts when the cache becomes unhealthy

Step 3: Heartbeat monitoring for cache warm-up jobs

If you pre-warm your cache on startup or on a schedule, use a heartbeat monitor to ensure the warm-up is completing successfully.

# lib/my_app/cache_warmer.ex
defmodule MyApp.CacheWarmer do
  use GenServer

  require Logger

  @cache :my_app_cache
  @warm_interval :timer.minutes(30)

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  @impl true
  def init(state) do
    send(self(), :warm)
    {:ok, state}
  end

  @impl true
  def handle_info(:warm, state) do
    case warm_cache() do
      :ok ->
        Logger.info("Cache warm-up completed successfully")
        ping_heartbeat()
      {:error, reason} ->
        Logger.error("Cache warm-up failed: #{inspect(reason)}")
        # No heartbeat ping → Vigilmon alerts after the window
    end

    schedule_warm()
    {:noreply, state}
  end

  defp schedule_warm, do: Process.send_after(self(), :warm, @warm_interval)

  defp warm_cache do
    with {:ok, records} <- MyApp.DataLoader.load_frequently_accessed(),
         :ok <- populate_cache(records) do
      :ok
    end
  end

  defp populate_cache(records) do
    Enum.each(records, fn record ->
      Cachex.put(@cache, record.id, record, ttl: :timer.hours(1))
    end)
    :ok
  end

  defp ping_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:cache_warmer_heartbeat_url]
    if url do
      case Req.get(url, receive_timeout: 5_000) do
        {:ok, _} -> :ok
        {:error, reason} -> Logger.warning("Heartbeat ping failed: #{inspect(reason)}")
      end
    end
  end
end

In Vigilmon:

  1. Click New Monitor → Heartbeat
  2. Set expected interval to 35 minutes (buffer above the 30-minute warm cycle)
  3. Copy the ping URL and set as VIGILMON_CACHE_WARMER_HEARTBEAT_URL
  4. Add to config/runtime.exs:
config :my_app, :vigilmon,
  cache_warmer_heartbeat_url: System.get_env("VIGILMON_CACHE_WARMER_HEARTBEAT_URL")

Step 4: TTL expiry monitoring with Cachex hooks

Cachex hooks let you observe expired and evicted entries. Use them to log cache churn and surface it in your health check.

# lib/my_app/cache_hook.ex
defmodule MyApp.CacheHook do
  use Cachex.Hook

  require Logger

  @impl true
  def execute({:expire, _args, result}, _state) do
    Logger.debug("Cache entry expired: #{inspect(result)}")
    {:ok, :ignore}
  end

  def execute({:del, _args, _result}, _state) do
    {:ok, :ignore}
  end

  def execute(_event, _state), do: {:ok, :ignore}
end

Register the hook when starting Cachex:

# lib/my_app/application.ex
children = [
  {Cachex, name: :my_app_cache, hooks: [MyApp.CacheHook]},
  # ...
]

Step 5: Alerts via Slack

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

Enable the Slack channel on all your Cachex monitors. You'll receive:

🔴 DOWN: yourdomain.com/cache/stats
Keyword check failed: "healthy":true not found in response
Detected from: EU-West, US-East

Step 6: Status page

  1. Go to Status Pages → New Status Page in Vigilmon
  2. Add your health monitor, cache stats monitor, and heartbeat monitor
  3. Share the public URL with your team

What you've built

| What | How | |------|-----| | Cache hit rate check | Cachex.stats/1 in health plug | | Cache size monitoring | Cachex.size/1 bounds check | | HTTP uptime monitoring | Vigilmon HTTP monitor → /health | | Keyword-based cache health | Vigilmon keyword check on /cache/stats | | Cache warm-up heartbeat | Heartbeat ping after successful warm cycle | | Entry expiry observability | Cachex hook for expired/evicted entries | | Slack downtime alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

Cachex keeps your reads fast. Vigilmon keeps external eyes on whether the cache is doing its job.


Next steps

  • Add hit rate trending to your health response so you can spot cache pollution after deploys
  • Use Vigilmon's response time history to correlate cache miss spikes with latency increases
  • Add a heartbeat monitor for each cache namespace if you have multiple Cachex instances
  • Monitor Cachex memory growth alongside OS memory — uncapped caches are a common OOM source

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 →