tutorial

How to Monitor Eredis with Vigilmon

Monitor Eredis connection pools, command latency, poolboy worker availability, and Redis reachability in your Elixir application with Vigilmon heartbeat and HTTP monitors.

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:

  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 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/1 with a latency timer
  • Alert on pool waiting count 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.

Monitor your app with Vigilmon

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

Start free →