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:
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval: 1 minute (paid) or 5 minutes (free)
- Save
For the pool stats endpoint, add a keyword monitor to detect pool exhaustion:
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/pool/stats - Under Keyword Check, add
"healthy":true - 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:
- Click New Monitor → Heartbeat
- Set expected interval to 2 minutes (buffer above the 1-minute check)
- 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.
- api.slack.com/apps → Create New App → From scratch
- Enable Incoming Webhooks → Add New Webhook
- 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
- Go to Status Pages → New Status Page in Vigilmon
- Add your health, pool stats, and downstream service monitors
- 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.