How to Monitor Redix with Vigilmon
Redix is a fast, lightweight Redis client for Elixir built on persistent connections and native pipeline support. Unlike connection pool wrappers, Redix gives you direct control over each connection's lifecycle — one process, one persistent TCP socket, pipelining by default. This makes it the go-to choice for high-throughput Redis operations in Phoenix applications where you need predictable latency and low overhead.
The tradeoff is operational transparency. A single Redix connection silently drops if the Redis server restarts, if a firewall rule closes idle TCP connections, or if network packet loss accumulates past the point Redix's TCP stack can absorb. Your process is alive, but commands hang or return connection errors.
Vigilmon lets you verify from outside that your Redix-backed operations are reachable and performant, not just that the Elixir process exists.
Why Monitor Redix?
| Failure mode | Why it's silent | |---|---| | Connection drops | Redix reconnects, but in-flight commands are lost | | Redis server restart | Reconnect race — commands fail until backoff resolves | | Pipeline stalls | A blocking command on one pipeline slot delays all subsequent commands | | Firewall idle-timeout | TCP is closed by a middlebox; Redix doesn't know until the next command | | Pub/Sub disconnection | Subscription process exits; your application stops receiving messages |
Key Metrics to Track
| Metric | What it tells you |
|--------|-------------------|
| Round-trip latency (PING) | End-to-end Redis reachability including network |
| Command error rate | AUTH failures, WRONGTYPE, NOAUTH, connection refused |
| Pipeline depth | Commands queued ahead of an in-flight pipeline |
| Reconnect frequency | How often Redix is re-establishing the TCP connection |
| Memory used by Redis | Eviction pressure that causes unexpected key misses |
| Blocked client count | Server-side stall on BLPOP, BRPOP, or WAIT |
Step 1: Add Redix to Your Application
# mix.exs
defp deps do
[
{:redix, "~> 1.4"}
]
end
Start a named Redix connection under your supervisor:
# lib/my_app/application.ex
children = [
{Redix, [
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"),
name: :redix,
sync_connect: false,
exit_on_disconnection: false,
backoff_initial: 500,
backoff_max: 30_000
]},
# ...
]
Step 2: Build a Redix Health Check
# lib/my_app/redix_health.ex
defmodule MyApp.RedixHealth do
require Logger
@timeout 5_000
@probe_key "vigilmon:health:probe"
def check do
with {:ping, {:ok, "PONG"}} <- {:ping, Redix.command(:redix, ["PING"], timeout: @timeout)},
{:write, {:ok, "OK"}} <- {:write, Redix.command(:redix, ["SET", @probe_key, "1", "EX", "30"], timeout: @timeout)},
{:read, {:ok, "1"}} <- {:read, Redix.command(:redix, ["GET", @probe_key], timeout: @timeout)},
{:info, {:ok, info_string}} <- {:info, Redix.command(:redix, ["INFO", "server"], timeout: @timeout)} do
%{
status: :ok,
ping_ok: true,
write_ok: true,
read_ok: true,
redis_version: parse_info(info_string, "redis_version")
}
else
{step, {:error, reason}} ->
Logger.warning("RedixHealth failed at #{step}: #{inspect(reason)}")
%{status: :error, failed_step: step, reason: inspect(reason)}
{step, unexpected} ->
%{status: :error, failed_step: step, reason: inspect(unexpected)}
end
end
def latency_ms do
start = System.monotonic_time(:millisecond)
case Redix.command(:redix, ["PING"], timeout: @timeout) do
{:ok, "PONG"} ->
{:ok, System.monotonic_time(:millisecond) - start}
{:error, reason} ->
{:error, reason}
end
end
defp parse_info(info_string, key) do
info_string
|> String.split("\r\n")
|> Enum.find_value(fn line ->
case String.split(line, ":") do
[^key, value] -> String.trim(value)
_ -> nil
end
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.RedixHealth.check()
{:ok, latency} = MyApp.RedixHealth.latency_ms()
status = if health.status == :ok, do: 200, else: 503
conn
|> put_status(status)
|> json(%{
status: if(status == 200, do: "ok", else: "degraded"),
redis: health,
latency_ms: latency
})
end
end
# lib/my_app_web/router.ex
scope "/", MyAppWeb do
get "/health", HealthController, :index
end
Step 4: Track Pipeline Throughput with Telemetry
# lib/my_app/redix_poller.ex
defmodule MyApp.RedixPoller 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 Redix.command(:redix, ["INFO", "stats"], timeout: 5_000) do
{:ok, info} ->
total_commands = parse_info(info, "total_commands_processed") |> to_integer()
instantaneous_ops = parse_info(info, "instantaneous_ops_per_sec") |> to_integer()
rejected = parse_info(info, "rejected_connections") |> to_integer()
:telemetry.execute(
[:my_app, :redix, :stats],
%{
total_commands: total_commands,
ops_per_sec: instantaneous_ops,
rejected_connections: rejected
},
%{}
)
{:error, reason} ->
Logger.warning("RedixPoller: failed to fetch INFO stats: #{inspect(reason)}")
end
case MyApp.RedixHealth.latency_ms() do
{:ok, ms} ->
:telemetry.execute([:my_app, :redix, :latency], %{ms: ms}, %{})
_ ->
:ok
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 it to your supervision tree:
children = [
{Redix, [...]},
MyApp.RedixPoller,
# ...
]
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 Redix liveness:
# lib/my_app/redix_heartbeat.ex
defmodule MyApp.RedixHeartbeat 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.RedixHealth.check()
if health.status == :ok do
url = System.get_env("VIGILMON_REDIX_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_REDIX_HEARTBEAT_URL to the heartbeat URL from the Vigilmon dashboard.
Step 6: Alerting
In Vigilmon, go to Notifications → New Channel and configure:
Slack for health degradation:
🔴 DOWN: Redix — MyApp
Monitor: /health returning redis.status = "degraded"
Step failed: {failed_step}
Action: Check Redis server status, Redix connection, network firewall rules
PagerDuty for heartbeat miss — a missed heartbeat means either the Redix connection is down and commands are failing, or the application itself is unreachable. Escalate immediately.
Common Redix Issues and Fixes
Connection drops under idle TCP timeout:
# Keep the connection alive with periodic PINGs
{Redix, [
host: "localhost",
name: :redix,
socket_opts: [keepalive: true]
]}
Commands timing out during Redis restart:
# Retry wrapper for transient connection errors
def command_with_retry(args, retries \\ 3) do
case Redix.command(:redix, args, timeout: 5_000) do
{:ok, result} ->
{:ok, result}
{:error, %Redix.ConnectionError{}} when retries > 0 ->
Process.sleep(200)
command_with_retry(args, retries - 1)
{:error, reason} ->
{:error, reason}
end
end
Pipeline deadlock on blocking commands:
# Use a dedicated connection for blocking commands
# Never mix BLPOP/BRPOP with regular commands on the same Redix process
{Redix, [name: :redix_blocking, host: "localhost"]}
What You've Built
| What | How | |------|-----| | Connection health check | PING + SET + GET round-trip verifying full Redis reachability | | Latency tracking | Monotonic timer around PING for end-to-end latency | | Stats telemetry | INFO stats parsed and emitted as telemetry events every 30s | | Structured health endpoint | JSON response with Redis status and latency | | Liveness heartbeat | GenServer pinging Vigilmon only when Redis is healthy | | Real-time alerting | Vigilmon HTTP monitor + PagerDuty on heartbeat miss |
Redix gives you raw Redis throughput. Vigilmon gives you confidence that the throughput is actually reaching Redis.
Next Steps
- Add per-command latency tracking using Redix's built-in telemetry events (
[:redix, :pipeline, :stop]) - Monitor Redis memory usage to catch eviction pressure before keys start disappearing
- Use Vigilmon's response time history to track Redis latency trends over days
- Set up a status page combining Redis health with your application's other data stores
Get started free at vigilmon.online.