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:
- 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
For the cache stats endpoint, add a keyword monitor:
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/cache/stats - Under Keyword Check, add
"healthy":true - 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:
- Click New Monitor → Heartbeat
- Set expected interval to 35 minutes (buffer above the 30-minute warm cycle)
- Copy the ping URL and set as
VIGILMON_CACHE_WARMER_HEARTBEAT_URL - 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
- Go to Status Pages → New Status Page in Vigilmon
- Add your health monitor, cache stats monitor, and heartbeat monitor
- 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.