tutorial

How to Monitor Redix with Vigilmon

Monitor Redix connection health, command latency, pipeline throughput, and Redis availability in your Elixir/Phoenix application with Vigilmon heartbeat and HTTP monitors.

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:

  1. Sign in at vigilmon.online
  2. Click New Monitor → HTTP
  3. URL: https://your-app.example.com/health
  4. Interval: 60 seconds
  5. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →