How to Monitor Thousand Island with Vigilmon
Thousand Island is a pure Elixir TCP server framework that forms the foundation of Bandit, the modern Elixir HTTP server. Where Bandit handles HTTP protocol specifics, Thousand Island handles everything beneath: socket acceptance, per-connection process spawning, backpressure, connection pooling, and graceful shutdown. If you're building a custom TCP protocol, a proxy, or any network service in Elixir, Thousand Island is the idiomatic choice.
Because Thousand Island manages the socket layer directly, failures here are silent and fundamental: the server process tree might look healthy from inside the BEAM while no new connections are being accepted from the network. You need external monitoring that actually opens TCP connections rather than relying on internal process checks.
This tutorial adds production observability to a Thousand Island application:
- A TCP health endpoint with status reporting
- External HTTP monitoring with Vigilmon
- Telemetry-driven connection and listener tracking
- Alerting on connection count anomalies
- Heartbeat monitoring for application-level workers
Step 1: Build a Thousand Island server with health reporting
Thousand Island handlers implement the ThousandIsland.Handler behaviour. Start with a minimal echo-style server that also exposes a health HTTP endpoint:
# mix.exs
defp deps do
[
{:thousand_island, "~> 1.0"},
{:plug, "~> 1.14"},
{:plug_cowboy, "~> 2.0"}, # or {:bandit, "~> 1.0"} for the health HTTP endpoint
{:jason, "~> 1.4"}
]
end
Define your protocol handler:
# lib/my_app/protocol_handler.ex
defmodule MyApp.ProtocolHandler do
use ThousandIsland.Handler
require Logger
@impl ThousandIsland.Handler
def handle_connection(socket, state) do
Logger.debug("New connection from #{inspect(ThousandIsland.Socket.peer_info(socket))}")
{:continue, state}
end
@impl ThousandIsland.Handler
def handle_data(data, socket, state) do
# Echo the data back (replace with your protocol logic)
ThousandIsland.Socket.send(socket, data)
{:continue, state}
end
@impl ThousandIsland.Handler
def handle_close(socket, _state) do
Logger.debug("Connection closed: #{inspect(ThousandIsland.Socket.peer_info(socket))}")
:ok
end
end
Add a separate HTTP health endpoint using Plug + Bandit (or Cowboy):
# lib/my_app/health_router.ex
defmodule MyApp.HealthRouter do
use Plug.Router
plug :match
plug :dispatch
get "/health" do
checks = %{
thousand_island: check_server(),
memory: check_memory()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
label = if status == 200, do: "ok", else: "degraded"
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(%{status: label, checks: checks}))
end
match _ do
send_resp(conn, 404, "Not found")
end
defp check_server do
case Process.whereis(MyApp.Server) do
nil -> :error
pid ->
if Process.alive?(pid), do: :ok, else: :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)
used_pct = (total - free) / total * 100
if used_pct < 90, do: :ok, else: :error
end
end
end
Wire everything up in your application supervisor:
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
port = String.to_integer(System.get_env("PORT") || "4000")
health_port = String.to_integer(System.get_env("HEALTH_PORT") || "4001")
children = [
# Your Thousand Island TCP server
{ThousandIsland, [
handler_module: MyApp.ProtocolHandler,
port: port,
num_acceptors: 100,
name: MyApp.Server
]},
# Separate HTTP endpoint for health checks
{Bandit, plug: MyApp.HealthRouter, port: health_port}
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
end
Test your health endpoint:
mix run --no-halt
curl http://localhost:4001/health
# {"status":"ok","checks":{"thousand_island":"ok","memory":"ok"}}
# Test TCP connectivity separately
nc -zv localhost 4000
# Connection to localhost 4000 port [tcp/*] succeeded!
Step 2: Attach to Thousand Island's telemetry
Thousand Island emits :telemetry events for the full connection lifecycle. Use these to track connection counts, per-connection latency, and error rates:
# lib/my_app/server_telemetry.ex
defmodule MyApp.ServerTelemetry do
require Logger
def attach do
:telemetry.attach_many(
"thousand-island-metrics",
[
[:thousand_island, :listener, :start],
[:thousand_island, :connection, :start],
[:thousand_island, :connection, :stop],
[:thousand_island, :connection, :error]
],
&handle_event/4,
nil
)
end
def handle_event([:thousand_island, :listener, :start], _measurements, metadata, _) do
Logger.info("Thousand Island listener started", port: metadata[:port], num_acceptors: metadata[:num_acceptors])
end
def handle_event([:thousand_island, :connection, :start], _measurements, metadata, _) do
Logger.debug("Connection established", remote_ip: inspect(metadata[:remote_ip]))
end
def handle_event([:thousand_island, :connection, :stop], measurements, _metadata, _) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.debug("Connection closed", duration_ms: duration_ms)
end
def handle_event([:thousand_island, :connection, :error], _measurements, metadata, _) do
Logger.warning("Connection error", error: inspect(metadata[:error]))
end
end
Start telemetry in your application:
def start(_type, _args) do
MyApp.ServerTelemetry.attach()
# ... rest of your children
end
Step 3: Enrich health checks with connection metrics
Expose live connection count and acceptor stats in your health response:
defp check_server do
case Process.whereis(MyApp.Server) do
nil -> :error
pid ->
if Process.alive?(pid) do
# ThousandIsland exposes connection info via its API
case ThousandIsland.connection_info(MyApp.Server) do
{:ok, %{num_connections: n}} when n >= 0 -> :ok
_ -> :ok # Connected but couldn't get stats — server still alive
end
else
:error
end
end
rescue
_ -> :error
end
Step 4: Set up HTTP monitoring in Vigilmon
Point Vigilmon at your health endpoint:
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
http://yourdomain.com:4001/health(or the public URL if behind a proxy) - Set check interval: 1 minute (paid) or 5 minutes (free)
- Add a keyword check for
"ok"to verify the health response - Save
Why Vigilmon's external probe matters for TCP servers:
An internal process check tells you the server process is alive. It does not tell you whether:
- The port is actually listening (firewall rule change, port conflict on restart)
- The acceptor pool is processing connections (pool saturation)
- The network path from clients to your server is intact
Vigilmon's probes open a real TCP connection through the network stack and verify the health endpoint, catching each of these failure modes independently.
Step 5: Add a TCP port monitor
For a second line of defense, add a raw TCP port monitor to Vigilmon:
- Click New Monitor → TCP
- Enter your server's hostname and TCP port (e.g.
yourdomain.com:4000) - Set check interval: 1 minute
- Save
This monitor verifies that Thousand Island is accepting TCP connections at the socket level, independently of your application-level health check. If the HTTP health check passes but the TCP monitor fails, you have a protocol-level issue rather than an application issue.
Step 6: Alerts via Slack
In Vigilmon, go to Notifications → New Channel → Slack and paste your incoming webhook URL.
Enable the notification channel on both monitors (HTTP health and TCP port). Example alert:
🔴 DOWN: yourdomain.com:4000 (TCP)
Status: Connection refused
Detected from: EU-West, US-East
1 minute ago
Configure separate alert channels for your HTTP health endpoint and TCP port monitor so you can distinguish application-level failures from network-level failures at a glance.
Step 7: Heartbeat monitoring for protocol workers
If Thousand Island handlers drive business processes (ingesting data, processing messages), add heartbeat monitoring:
# lib/my_app/protocol_handler.ex
defmodule MyApp.ProtocolHandler do
use ThousandIsland.Handler
require Logger
@impl ThousandIsland.Handler
def handle_data(data, socket, state) do
case process_message(data) do
{:ok, response} ->
ThousandIsland.Socket.send(socket, response)
ping_heartbeat()
{:continue, state}
{:error, reason} ->
Logger.error("Message processing failed: #{inspect(reason)}")
{:continue, state}
end
end
defp process_message(data), do: {:ok, data} # your logic
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:protocol_heartbeat_url]
if url, do: Task.start(fn -> Req.get(url, receive_timeout: 5_000) end)
end
end
Add config:
# config/runtime.exs
config :my_app, :vigilmon,
protocol_heartbeat_url: System.get_env("VIGILMON_PROTOCOL_HEARTBEAT_URL")
In Vigilmon:
- New Monitor → Heartbeat
- Set the expected ping interval to slightly above your message rate
- Copy the ping URL → set as
VIGILMON_PROTOCOL_HEARTBEAT_URL
Step 8: Status page
- Status Pages → New Status Page in Vigilmon
- Add your HTTP health monitor, TCP port monitor, and heartbeat monitors
- Share the public URL in your
READMEand internal runbooks
README badge:

What you've built
| What | How |
|------|-----|
| Health check endpoint | HealthRouter Plug with process liveness check |
| Telemetry instrumentation | [:thousand_island, :connection, :*] handlers |
| HTTP health monitoring | Vigilmon HTTP monitor → :4001/health |
| TCP port monitoring | Vigilmon TCP monitor → :4000 |
| Multi-region probes | Vigilmon multi-probe checks from global locations |
| Slack downtime alerts | Vigilmon Slack notification channel |
| Protocol worker heartbeats | Heartbeat ping on successful message processing |
| Status page | Vigilmon public status page |
Thousand Island manages your sockets. Vigilmon verifies those sockets are reachable from the network.
Next steps
- Export
[:thousand_island, :connection, :stop]duration metrics to Prometheus or StatsD for per-connection latency histograms - Use Vigilmon's response time history to detect acceptor pool saturation before it causes client-visible timeouts
- Add TCP monitors for every port your Thousand Island server listens on, not just the primary one
- Set up escalation policies in Vigilmon so TCP failures page on-call immediately while HTTP health degradations alert the team channel
Get started free at vigilmon.online.