tutorial

How to Monitor ex_redis with Vigilmon

Monitor ex_redis command health, Eredis connection pool status, key expiry, and Redis availability in your Elixir application with Vigilmon heartbeat and HTTP monitors.

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:

  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 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.

Monitor your app with Vigilmon

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

Start free →