How to Monitor ex_redis with Vigilmon
ex_redis is an Elixir wrapper around Eredis that provides a higher-level, more idiomatic interface for Redis operations. Where Eredis gives you raw tuples and charlist-based APIs, ex_redis wraps these into a cleaner Elixir interface — making Redis commands feel native in your codebase while still using Eredis under the hood for connection management and pooling.
Applications that use ex_redis for session storage, caching, and rate limiting depend on it being both alive and performant. A connection drop that Eredis silently retries in the background still causes ex_redis command failures in the meantime. A pool exhaustion event leaves session writes failing while your process tree looks healthy.
Vigilmon gives you external confirmation that ex_redis is processing commands end-to-end — not just that the GenServer tree is alive.
Why Monitor ex_redis?
| Failure mode | Why it's silent |
|---|---|
| Eredis backend drop | ex_redis returns {:error, reason} but no process crashes |
| Session write failures | Users get logged out; no exception propagates to the caller |
| Cache key eviction | Reads return nil; application recomputes silently |
| Pool saturation | ex_redis calls time out; only the calling process sees it |
| Auth misconfiguration | Commands return {:error, "NOAUTH"} after connection opens |
Key Metrics to Track
| Metric | What it tells you |
|--------|-------------------|
| Command success rate | How often ex_redis calls return :ok vs error tuples |
| Round-trip latency | End-to-end Redis reachability from Elixir |
| Session key TTL accuracy | Whether session expiry is being set correctly |
| Cache hit vs miss ratio | How effectively ex_redis-backed caching is working |
| Pool availability | Available Eredis workers for ex_redis to use |
| Key count in monitored namespaces | Unexpected growth or drain in session/cache keys |
Step 1: Add ex_redis to Your Application
# mix.exs
defp deps do
[
{:ex_redis, "~> 0.1"},
# ex_redis depends on eredis and poolboy
{:eredis, "~> 1.7"},
{:poolboy, "~> 1.5"}
]
end
Configure and start ex_redis:
# config/config.exs
config :ex_redis,
host: System.get_env("REDIS_HOST", "127.0.0.1"),
port: String.to_integer(System.get_env("REDIS_PORT", "6379")),
password: System.get_env("REDIS_PASSWORD", ""),
pool_size: 10,
pool_max_overflow: 5
# lib/my_app/application.ex
children = [
ExRedis,
# ...
]
Step 2: Build a Health Check
# lib/my_app/ex_redis_health.ex
defmodule MyApp.ExRedisHealth do
require Logger
@probe_key "vigilmon:health:probe"
@probe_value "health_ok"
@session_prefix "session:"
@cache_prefix "cache:"
def check do
start = System.monotonic_time(:millisecond)
with {:ping, :ok} <- {:ping, check_ping()},
{:write, :ok} <- {:write, write_probe()},
{:read, :ok} <- {:read, read_probe()},
{:ttl, :ok} <- {:ttl, check_ttl()},
{:delete, _} <- {:delete, ExRedis.del(@probe_key)} do
latency = System.monotonic_time(:millisecond) - start
%{
status: :ok,
latency_ms: latency,
session_keys: count_keys(@session_prefix),
cache_keys: count_keys(@cache_prefix)
}
else
{step, {:error, reason}} ->
Logger.warning("ExRedisHealth failed at #{step}: #{inspect(reason)}")
%{status: :error, failed_step: step, reason: to_string(reason)}
{step, :error} ->
%{status: :error, failed_step: step, reason: "unexpected nil or error response"}
end
rescue
e ->
%{status: :error, reason: inspect(e)}
end
defp check_ping do
case ExRedis.command(["PING"]) do
{:ok, "PONG"} -> :ok
{:ok, other} -> {:error, "unexpected PING response: #{other}"}
error -> error
end
end
defp write_probe do
case ExRedis.command(["SET", @probe_key, @probe_value, "EX", "60"]) do
{:ok, "OK"} -> :ok
error -> error
end
end
defp read_probe do
case ExRedis.command(["GET", @probe_key]) do
{:ok, @probe_value} -> :ok
{:ok, nil} -> {:error, "probe key not found after write"}
{:ok, other} -> {:error, "probe read mismatch: #{other}"}
error -> error
end
end
defp check_ttl do
case ExRedis.command(["TTL", @probe_key]) do
{:ok, ttl} when is_integer(ttl) and ttl > 0 -> :ok
{:ok, -1} -> {:error, "probe key has no TTL — expiry not being set"}
{:ok, -2} -> {:error, "probe key expired before TTL check"}
error -> error
end
end
defp count_keys(prefix) do
case ExRedis.command(["KEYS", "#{prefix}*"]) do
{:ok, keys} when is_list(keys) -> length(keys)
_ -> nil
end
end
end
Step 3: Expose a Health Endpoint
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def index(conn, _params) do
health = MyApp.ExRedisHealth.check()
status = if health.status == :ok, do: 200, else: 503
conn
|> put_status(status)
|> json(%{
status: if(status == 200, do: "ok", else: "degraded"),
redis: health
})
end
end
# lib/my_app_web/router.ex
scope "/", MyAppWeb do
get "/health", HealthController, :index
end
Step 4: Track Session and Cache Metrics
# lib/my_app/ex_redis_poller.ex
defmodule MyApp.ExRedisPoller do
use GenServer
require Logger
@interval :timer.seconds(60)
@session_prefix "session:"
@cache_prefix "cache:"
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def init(state) do
schedule()
{:ok, state}
end
def handle_info(:poll, state) do
report_metrics()
schedule()
{:noreply, state}
end
defp report_metrics do
session_count = count_keys(@session_prefix)
cache_count = count_keys(@cache_prefix)
:telemetry.execute(
[:my_app, :ex_redis, :key_counts],
%{session_keys: session_count, cache_keys: cache_count},
%{}
)
case ExRedis.command(["INFO", "stats"]) do
{:ok, info} ->
keyspace_hits = parse_info(info, "keyspace_hits") |> to_integer()
keyspace_misses = parse_info(info, "keyspace_misses") |> to_integer()
hit_rate =
if keyspace_hits + keyspace_misses > 0,
do: Float.round(keyspace_hits / (keyspace_hits + keyspace_misses) * 100, 2),
else: 0.0
:telemetry.execute(
[:my_app, :ex_redis, :hit_rate],
%{hits: keyspace_hits, misses: keyspace_misses, rate_pct: hit_rate},
%{}
)
{:error, reason} ->
Logger.warning("ExRedisPoller: INFO stats failed: #{inspect(reason)}")
end
end
defp count_keys(prefix) do
case ExRedis.command(["KEYS", "#{prefix}*"]) do
{:ok, keys} when is_list(keys) -> length(keys)
_ -> 0
end
end
defp parse_info(info, key) do
info
|> String.split("\r\n")
|> Enum.find_value("0", fn line ->
case String.split(line, ":") do
[^key, value] -> String.trim(value)
_ -> nil
end
end)
end
defp to_integer(s) do
case Integer.parse(s) do
{n, _} -> n
:error -> 0
end
end
defp schedule, do: Process.send_after(self(), :poll, @interval)
end
Add to your supervision tree:
children = [
ExRedis,
MyApp.ExRedisPoller,
# ...
]
Step 5: Create Monitors in Vigilmon
HTTP monitor for the health endpoint:
- Sign in at vigilmon.online
- Click New Monitor → HTTP
- URL:
https://your-app.example.com/health - Interval: 60 seconds
- Alert condition: non-200 or body contains
"status":"degraded"
Heartbeat monitor for ex_redis liveness:
# lib/my_app/ex_redis_heartbeat.ex
defmodule MyApp.ExRedisHeartbeat do
use GenServer
@interval :timer.minutes(2)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def init(state) do
schedule()
{:ok, state}
end
def handle_info(:ping, state) do
health = MyApp.ExRedisHealth.check()
if health.status == :ok do
url = System.get_env("VIGILMON_EX_REDIS_HEARTBEAT_URL")
if url do
:httpc.request(:get, {String.to_charlist(url), []}, [{:timeout, 5_000}], [])
end
end
schedule()
{:noreply, state}
end
defp schedule, do: Process.send_after(self(), :ping, @interval)
end
In Vigilmon, create a Heartbeat monitor with a 5-minute expected interval. Set VIGILMON_EX_REDIS_HEARTBEAT_URL to the heartbeat URL from the Vigilmon dashboard.
Step 6: Alerting
In Vigilmon, go to Notifications → New Channel and configure:
Slack for Redis degradation:
🔴 DOWN: ex_redis — MyApp
Monitor: /health returning redis.status = "degraded"
Step failed: {failed_step}
Action: Check Redis server, Eredis pool availability, AUTH config, firewall rules
Slack for session key drain alert (watch your poller metrics):
⚠️ WARN: Session key count dropped — MyApp
Current session keys: {session_keys}
Action: Check for Redis eviction, Redis restart clearing keys, or session write failures
PagerDuty for heartbeat miss — ex_redis is not processing commands; sessions and cache are both unavailable.
Common ex_redis Issues and Fixes
Session keys disappearing after Redis restart:
# Verify persistence is configured in redis.conf
# appendonly yes
# appendfsync everysec
# Verify from IEx
{:ok, persist} = ExRedis.command(["CONFIG", "GET", "appendonly"])
IO.inspect(persist) # Should be ["appendonly", "yes"]
NOAUTH errors after config change:
# Verify the password is set in your runtime config, not compile-time
# config/runtime.exs (not config/config.exs)
config :ex_redis, password: System.get_env("REDIS_PASSWORD", "")
Key expiry not being set on session writes:
# Always set EX or PX on session writes — never use plain SET for sessions
def put_session(key, value, ttl_seconds \\ 3600) do
case ExRedis.command(["SET", "session:#{key}", :erlang.term_to_binary(value), "EX", to_string(ttl_seconds)]) do
{:ok, "OK"} -> :ok
{:error, reason} -> {:error, reason}
end
end
Unexpected nil returns for keys that should exist:
# Check if Redis is evicting under memory pressure
{:ok, maxmemory} = ExRedis.command(["CONFIG", "GET", "maxmemory"])
{:ok, policy} = ExRedis.command(["CONFIG", "GET", "maxmemory-policy"])
IO.inspect({maxmemory, policy})
# If policy is "allkeys-lru" and maxmemory is low, increase maxmemory
What You've Built
| What | How | |------|-----| | Command health check | PING + SET + GET + TTL verification through ex_redis | | Session and cache key counts | KEYS scan per namespace reported as telemetry | | Cache hit rate tracking | INFO stats parsed and emitted every 60s | | TTL accuracy check | Verified probe key has positive TTL after write | | Liveness heartbeat | GenServer pinging Vigilmon only when ex_redis is healthy | | Real-time alerting | HTTP monitor + PagerDuty on heartbeat miss |
ex_redis gives you idiomatic Elixir patterns over Eredis. Vigilmon gives you external proof that those patterns are actually reaching Redis.
Next Steps
- Add per-namespace TTL monitoring to catch session or cache keys that were written without expiry
- Alert on keyspace hit rate dropping below a threshold as an early warning of cache bypass
- Use Vigilmon response time history to track Redis latency trends and catch degradation before users notice
- Set up a status page combining Redis health with your database, background job, and external API monitors
Get started free at vigilmon.online.