How to Monitor Eredis with Vigilmon
Eredis is a low-level Erlang Redis client that gives you direct control over Redis connections and connection pooling. Unlike higher-level clients that manage pools for you, Eredis lets you pair it with poolboy to define exactly how many connections you want, how they behave when the pool is saturated, and how to handle reconnection. This makes Eredis a popular choice in Elixir applications that need fine-grained Redis control without the overhead of a full framework.
The tradeoff is visibility. When a poolboy checkout times out, when all Redis connections are in use, or when the Eredis client process crashes and is restarted by its supervisor, your application knows — but nothing outside your application does.
Vigilmon gives you external confirmation that Eredis is actually processing commands and the connection pool is not exhausted, independent of what your internal process tree believes.
Why Monitor Eredis?
| Failure mode | Why it's silent |
|---|---|
| Pool exhaustion | poolboy:checkout/2 times out; callers block or crash |
| Eredis client crash | Supervisor restarts it, but in-flight commands are dropped |
| Redis auth failure | Connection succeeds; first command returns {error, <<"ERR …">>} |
| TCP keepalive gap | Middlebox silently drops idle TCP; next command hangs until timeout |
| Redis server restart | Eredis reconnects, but commands during reconnect fail |
Key Metrics to Track
| Metric | What it tells you | |--------|-------------------| | Pool checkout latency | How long callers wait for a connection; rising = pool pressure | | Available workers | Workers ready in pool; 0 = fully saturated | | Command error rate | Auth, protocol, or timeout errors from Redis commands | | Reconnect events | How often Eredis is re-establishing connections | | Round-trip latency (PING) | End-to-end Redis reachability | | Redis memory used | Eviction risk for cached keys |
Step 1: Add Eredis and Poolboy
# mix.exs
defp deps do
[
{:eredis, "~> 1.7"},
{:poolboy, "~> 1.5"}
]
end
Define a poolboy pool of Eredis workers:
# lib/my_app/redis_pool.ex
defmodule MyApp.RedisPool do
def child_spec(_opts) do
pool_opts = [
name: {:local, :redis_pool},
worker_module: MyApp.RedisWorker,
size: 10,
max_overflow: 5
]
worker_args = [
host: String.to_charlist(System.get_env("REDIS_HOST", "127.0.0.1")),
port: String.to_integer(System.get_env("REDIS_PORT", "6379")),
password: String.to_charlist(System.get_env("REDIS_PASSWORD", "")),
reconnect_sleep: 500
]
:poolboy.child_spec(:redis_pool, pool_opts, worker_args)
end
end
# lib/my_app/redis_worker.ex
defmodule MyApp.RedisWorker do
use GenServer
def start_link(args) do
GenServer.start_link(__MODULE__, args)
end
def init(args) do
host = Keyword.get(args, :host, ~c"127.0.0.1")
port = Keyword.get(args, :port, 6379)
password = Keyword.get(args, :password, ~c"")
reconnect = Keyword.get(args, :reconnect_sleep, 500)
opts = [{:host, host}, {:port, port}, {:reconnect_sleep, reconnect}]
opts = if password != ~c"", do: [{:password, password} | opts], else: opts
{:ok, client} = :eredis.start_link(opts)
{:ok, client}
end
def handle_call({:q, args}, _from, client) do
result = :eredis.q(client, args)
{:reply, result, client}
end
def handle_call({:qp, pipeline}, _from, client) do
result = :eredis.qp(client, pipeline)
{:reply, result, client}
end
end
# lib/my_app/redis.ex — convenience wrapper
defmodule MyApp.Redis do
@timeout 5_000
def command(args) do
:poolboy.transaction(:redis_pool, fn worker ->
GenServer.call(worker, {:q, args}, @timeout)
end, @timeout)
end
def pipeline(commands) do
:poolboy.transaction(:redis_pool, fn worker ->
GenServer.call(worker, {:qp, commands}, @timeout)
end, @timeout)
end
end
# lib/my_app/application.ex
children = [
MyApp.RedisPool,
# ...
]
Step 2: Build a Health Check
# lib/my_app/eredis_health.ex
defmodule MyApp.EredisHealth do
require Logger
@probe_key "vigilmon:health:probe"
@timeout 5_000
def check do
start = System.monotonic_time(:millisecond)
with {:ping, {:ok, "PONG"}} <- {:ping, MyApp.Redis.command(["PING"])},
{:write, {:ok, "OK"}} <- {:write, MyApp.Redis.command(["SET", @probe_key, "1", "EX", "30"])},
{:read, {:ok, "1"}} <- {:read, MyApp.Redis.command(["GET", @probe_key])},
{:pool, pool_status} <- {:pool, pool_status()} do
latency = System.monotonic_time(:millisecond) - start
%{
status: :ok,
latency_ms: latency,
pool: pool_status
}
else
{:ping, {:error, reason}} ->
%{status: :error, failed_step: :ping, reason: to_string(reason)}
{step, {:error, reason}} ->
Logger.warning("EredisHealth failed at #{step}: #{inspect(reason)}")
%{status: :error, failed_step: step, reason: to_string(reason)}
{step, unexpected} ->
%{status: :error, failed_step: step, reason: inspect(unexpected)}
end
rescue
e ->
%{status: :error, reason: inspect(e)}
end
defp pool_status do
case :poolboy.status(:redis_pool) do
{_state, workers, overflow, waiting} ->
%{available: workers, overflow_used: overflow, waiting: waiting}
_ ->
%{available: :unknown}
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.EredisHealth.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 Pool Metrics with Telemetry
# lib/my_app/eredis_poller.ex
defmodule MyApp.EredisPoller do
use GenServer
require Logger
@interval :timer.seconds(30)
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
case :poolboy.status(:redis_pool) do
{_state, available, overflow, waiting} ->
:telemetry.execute(
[:my_app, :redis_pool, :status],
%{available: available, overflow_used: overflow, waiting_callers: waiting},
%{}
)
_ ->
Logger.warning("EredisPoller: could not fetch pool status")
end
case MyApp.Redis.command(["INFO", "stats"]) do
{:ok, info_string} ->
ops = parse_info(info_string, "instantaneous_ops_per_sec") |> to_integer()
rejected = parse_info(info_string, "rejected_connections") |> to_integer()
:telemetry.execute(
[:my_app, :redis, :stats],
%{ops_per_sec: ops, rejected_connections: rejected},
%{}
)
{:error, reason} ->
Logger.warning("EredisPoller: INFO stats failed: #{inspect(reason)}")
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 = [
MyApp.RedisPool,
MyApp.EredisPoller,
# ...
]
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 Eredis liveness:
# lib/my_app/eredis_heartbeat.ex
defmodule MyApp.EredisHeartbeat 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.EredisHealth.check()
if health.status == :ok do
url = System.get_env("VIGILMON_EREDIS_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_EREDIS_HEARTBEAT_URL to the heartbeat URL from the Vigilmon dashboard.
Step 6: Alerting
In Vigilmon, go to Notifications → New Channel and configure:
Slack for connection degradation:
🔴 DOWN: Eredis — MyApp
Monitor: /health returning redis.status = "degraded"
Step failed: {failed_step}
Action: Check Redis server, poolboy pool exhaustion (waiting callers), firewall rules
PagerDuty for heartbeat miss — Eredis is not processing commands; all Redis-dependent features are failing.
Common Eredis Issues and Fixes
Pool exhaustion under load:
# Detect approaching exhaustion before callers block
defp report_metrics do
case :poolboy.status(:redis_pool) do
{_, available, overflow, waiting} when waiting > 0 ->
Logger.warning("Redis pool has #{waiting} waiting callers")
:telemetry.execute([:my_app, :redis_pool, :exhaustion], %{waiting: waiting}, %{})
_ ->
:ok
end
end
Eredis error tuples from Redis auth failure:
# Eredis returns {:error, <<"NOAUTH Authentication required">>}
# Convert to readable strings for logging
defp handle_command_result({:ok, value}), do: {:ok, value}
defp handle_command_result({:error, reason}) when is_binary(reason), do: {:error, reason}
defp handle_command_result({:error, reason}), do: {:error, to_string(reason)}
Reconnect storms after Redis restart:
# Stagger reconnect with jitter to prevent thundering herd
worker_args = [
host: ~c"localhost",
port: 6379,
reconnect_sleep: :rand.uniform(1_000) + 500
]
What You've Built
| What | How | |------|-----| | Connection health check | PING + SET + GET through poolboy round-trip | | Pool status monitoring | Available workers, overflow usage, waiting callers | | Latency tracking | Monotonic timer around full health probe | | Redis stats telemetry | INFO stats parsed and emitted every 30s | | Liveness heartbeat | GenServer pinging Vigilmon only when Eredis is healthy | | Real-time alerting | HTTP monitor + PagerDuty on heartbeat miss |
Eredis gives you low-level control over every Redis connection. Vigilmon gives you external proof that those connections are actually serving traffic.
Next Steps
- Add per-command telemetry by wrapping
MyApp.Redis.command/1with a latency timer - Alert on pool
waitingcount crossing a threshold as an early warning before timeouts - Use Vigilmon response time history to correlate Redis latency with application request latency
- Set up a status page that combines Redis health with your Ecto database and background job monitors
Get started free at vigilmon.online.