tutorial

How to Monitor Poolboy with Vigilmon

Add health checks, uptime monitoring, and alerts to Elixir/Erlang apps using the Poolboy worker pool library — catch pool exhaustion and worker crashes before they affect users.

How to Monitor Poolboy with Vigilmon

Poolboy is the battle-tested worker pool library for Erlang and Elixir. It sits underneath Ecto's connection pooling, many external API clients, and custom concurrency workloads. When Poolboy is healthy, your application handles concurrent load gracefully. When it degrades — pool exhaustion, worker crashes, checkout timeouts — the failure is often invisible until users start seeing errors.

This tutorial shows you how to instrument Poolboy pools and monitor them with Vigilmon:

  • A health endpoint that reports pool saturation and availability
  • Uptime monitoring for the services your workers connect to
  • Heartbeat monitoring to detect silent worker failure
  • Slack alerts when pools exhaust or workers stop delivering

Step 1: Expose pool metrics in a health endpoint

Poolboy's :poolboy.status/1 returns {:ready, :overflow, :monitors, :size} — this is the raw material for your health check.

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = run_checks()
    status = if all_ok?(checks), do: 200, else: 503

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(%{status: status_label(status), checks: checks}))
    |> halt()
  end

  def call(conn, _opts), do: conn

  defp run_checks do
    %{
      database: check_database(),
      worker_pool: check_worker_pool(),
      memory: check_memory()
    }
  end

  defp check_worker_pool do
    pool_name = MyApp.WorkerPool

    try do
      {_state, workers, overflow, monitors} = :poolboy.status(pool_name)
      pool_size = Application.get_env(:my_app, :worker_pool_size, 10)
      available = workers
      in_use = monitors
      overflow_active = overflow

      cond do
        available == 0 and overflow_active >= pool_size -> :exhausted
        available < 2 -> :degraded
        true -> :ok
      end
    catch
      :exit, _ -> :error
    end
  end

  defp check_database do
    case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
      {:ok, _} -> :ok
      {:error, _} -> :error
    end
  end

  defp check_memory do
    case :memsup.get_system_memory_data() do
      [] -> :ok
      data ->
        total = Keyword.get(data, :total_memory, 1)
        free = Keyword.get(data, :free_memory, total)
        if (total - free) / total * 100 < 90, do: :ok, else: :error
    end
  end

  defp all_ok?(checks) do
    Enum.all?(checks, fn {_, v} -> v in [:ok, :degraded] end)
  end

  defp status_label(200), do: "ok"
  defp status_label(_), do: "degraded"
end

Add a richer pool stats endpoint to expose metrics for dashboards:

# lib/my_app_web/controllers/pool_stats_controller.ex
defmodule MyAppWeb.PoolStatsController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    stats = pool_stats(MyApp.WorkerPool)
    json(conn, stats)
  end

  defp pool_stats(pool_name) do
    try do
      {state, workers, overflow, monitors} = :poolboy.status(pool_name)
      %{
        state: state,
        available_workers: workers,
        overflow_workers: overflow,
        checked_out: monitors,
        healthy: state == :ready
      }
    catch
      :exit, reason -> %{error: inspect(reason), healthy: false}
    end
  end
end

Wire up the plug and route in your endpoint/router:

# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck

# lib/my_app_web/router.ex
get "/pool/stats", PoolStatsController, :index

Step 2: Set up HTTP monitoring in Vigilmon

Point Vigilmon at your health endpoint:

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute (paid) or 5 minutes (free)
  5. Save

For the pool stats endpoint, add a keyword monitor to detect pool exhaustion:

  1. Click New Monitor → HTTP
  2. Enter https://yourdomain.com/pool/stats
  3. Under Keyword Check, add "healthy":true
  4. If the keyword is absent (pool degraded), Vigilmon alerts

Step 3: Heartbeat monitoring for worker jobs

Poolboy workers that process jobs can fail silently — the pool stays up, workers check out and check in, but the actual work stops happening. Heartbeat monitoring catches this.

# lib/my_app/workers/api_worker.ex
defmodule MyApp.Workers.ApiWorker do
  require Logger

  def process(request) do
    :poolboy.transaction(MyApp.WorkerPool, fn worker ->
      result = GenServer.call(worker, {:process, request})

      case result do
        {:ok, _} ->
          ping_heartbeat()
          result
        {:error, reason} ->
          Logger.error("Worker failed: #{inspect(reason)}")
          result
      end
    end, 5_000)
  end

  defp ping_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:worker_heartbeat_url]
    if url do
      case Req.get(url, receive_timeout: 5_000) do
        {:ok, _} -> :ok
        {:error, reason} -> Logger.warning("Heartbeat ping failed: #{inspect(reason)}")
      end
    end
  end
end

Add a periodic heartbeat ping from a GenServer that verifies the pool is actively processing:

# lib/my_app/pool_monitor.ex
defmodule MyApp.PoolMonitor do
  use GenServer

  require Logger

  @check_interval :timer.minutes(1)

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  @impl true
  def init(state) do
    schedule_check()
    {:ok, state}
  end

  @impl true
  def handle_info(:check, state) do
    case :poolboy.status(MyApp.WorkerPool) do
      {:ready, workers, _overflow, _monitors} when workers > 0 ->
        ping_heartbeat()
      _ ->
        Logger.warning("Worker pool degraded — skipping heartbeat")
    end

    schedule_check()
    {:noreply, state}
  end

  defp schedule_check, do: Process.send_after(self(), :check, @check_interval)

  defp ping_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:pool_monitor_heartbeat_url]
    if url, do: Req.get(url, receive_timeout: 5_000)
  end
end

In Vigilmon:

  1. Click New Monitor → Heartbeat
  2. Set expected interval to 2 minutes (buffer above the 1-minute check)
  3. Copy the ping URL and set it as VIGILMON_POOL_MONITOR_HEARTBEAT_URL

Step 4: Monitor the downstream services your workers connect to

Poolboy manages concurrency, but workers typically connect to external services: APIs, databases, Redis, S3. Monitor these directly so you know whether pool problems stem from external failures.

defmodule MyAppWeb.Plugs.HealthCheck do
  # ... extend run_checks/0

  defp run_checks do
    %{
      database: check_database(),
      worker_pool: check_worker_pool(),
      external_api: check_external_api(),
      memory: check_memory()
    }
  end

  defp check_external_api do
    url = Application.get_env(:my_app, :external_api_url)
    case Req.get("#{url}/health", receive_timeout: 3_000) do
      {:ok, %{status: 200}} -> :ok
      _ -> :error
    end
  end
end

Add a separate HTTP monitor in Vigilmon for the downstream service URL. When this monitor goes down before your app health check does, you know the failure is external.


Step 5: Alerts via Slack

In Vigilmon, go to Notifications → New Channel → Slack and paste your Slack webhook URL.

  1. api.slack.com/appsCreate New App → From scratch
  2. Enable Incoming WebhooksAdd New Webhook
  3. Pick your alerts channel and copy the URL

Enable the Slack channel on your pool monitors. You'll receive:

🔴 DOWN: yourdomain.com/health
Keyword check failed: "healthy":true not found in response
Detected from: EU-West, US-East

Step 6: Status page

  1. Go to Status Pages → New Status Page in Vigilmon
  2. Add your health, pool stats, and downstream service monitors
  3. Share the public URL with your team

What you've built

| What | How | |------|-----| | Pool saturation check | :poolboy.status/1 in health plug | | HTTP uptime monitoring | Vigilmon HTTP monitor → /health | | Keyword-based pool alert | Vigilmon keyword check on /pool/stats | | Worker job heartbeat | Heartbeat ping on successful process/1 | | Pool monitor heartbeat | PoolMonitor GenServer pings every minute | | Downstream service monitoring | Separate Vigilmon HTTP monitor per dependency | | Slack downtime alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

Poolboy keeps your concurrency safe. Vigilmon keeps external eyes on whether the work is actually happening.


Next steps

  • Add pool size metrics to your health response so Vigilmon can track saturation trends over time
  • Use Vigilmon's response time history to spot when pool checkout latency increases before timeouts occur
  • Add separate heartbeat monitors for each worker queue type
  • Monitor pool overflow separately — overflow workers are a leading indicator of capacity problems

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 →