NATS is a high-performance, cloud-native messaging system that powers microservice communication, IoT telemetry, and distributed data pipelines. Its design philosophy — at-most-once delivery by default, with optional persistence via JetStream — makes it fast and lightweight, but also means silent failures are the norm rather than the exception. A NATS server partition doesn't return an error to publishers; messages are simply dropped. A stalled JetStream consumer doesn't raise an exception; events just pile up in the stream.
Vigilmon gives you external visibility into NATS server health through HTTP probe monitoring of NATS's built-in monitoring port and heartbeat monitoring for your subscriber applications. This tutorial walks through setting up both.
Why NATS Needs External Monitoring
NATS exposes rich internal health data via its built-in monitoring HTTP port (default 8222). The problem is that you need to be watching those endpoints for that data to be actionable. External monitoring with Vigilmon adds:
- Proactive alerting when NATS server health endpoints return non-200 or unexpected data
- JetStream consumer lag detection via the consumer info API
- Heartbeat monitoring so you know immediately when a subscriber application stops processing messages — even when the NATS server itself is healthy
- Multi-region availability checking that confirms NATS is reachable from outside your cluster, not just from within
Step 1: Use NATS's Built-in Monitoring Endpoints
NATS ships with a built-in HTTP monitoring server. Enable it by starting NATS with:
nats-server --http_port 8222
Or in nats-server.conf:
http_port: 8222
The monitoring port exposes these endpoints you can probe directly with Vigilmon:
# Server health and version
curl http://nats-host:8222/healthz
# {"status":"ok","now":"2026-07-01T12:00:00Z"}
# Server info (connections, subscriptions, uptime)
curl http://nats-host:8222/varz
# JetStream account summary
curl http://nats-host:8222/jsz
# Cluster routing info
curl http://nats-host:8222/routez
# Server connections
curl http://nats-host:8222/connz
The /healthz endpoint returns {"status":"ok"} when the server is healthy and responds with a non-200 status during initialization or degraded states. Point your first Vigilmon monitor at this endpoint.
Step 2: Configure Vigilmon HTTP Monitors for NATS
Monitor 1: NATS Server Liveness
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to:
http://nats-host:8222/healthz - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
2000ms
- Status code:
- Under Alert channels, assign your Slack or PagerDuty channel
- Save the monitor
Monitor 2: JetStream Health Check
Build a thin sidecar to check JetStream stream and consumer health:
// health/nats.js
const express = require('express');
const { connect, StringCodec } = require('nats');
const app = express();
const sc = StringCodec();
app.get('/health/nats/jetstream', async (req, res) => {
let nc;
try {
nc = await connect({
servers: process.env.NATS_URL || 'nats://localhost:4222',
timeout: 3000,
});
const js = nc.jetstream();
const jsm = await nc.jetstreamManager();
// Check that the stream exists and is not degraded
const streamName = process.env.NATS_STREAM || 'events';
const info = await jsm.streams.info(streamName);
const state = info.state;
// Check consumer lag — alert if pending messages exceed threshold
const consumers = await jsm.consumers.list(streamName).next();
let maxPending = 0;
for (const consumer of consumers) {
maxPending = Math.max(maxPending, consumer.num_pending || 0);
}
const lagThreshold = parseInt(process.env.NATS_LAG_THRESHOLD || '1000');
if (maxPending > lagThreshold) {
await nc.close();
return res.status(503).json({
status: 'degraded',
reason: 'consumer_lag',
max_pending: maxPending,
threshold: lagThreshold,
});
}
await nc.close();
return res.status(200).json({
status: 'ok',
stream: streamName,
messages: state.messages,
max_pending: maxPending,
});
} catch (err) {
if (nc) await nc.close().catch(() => {});
return res.status(503).json({ status: 'down', error: err.message });
}
});
app.listen(3003);
Add a second Vigilmon monitor for this endpoint:
- URL:
https://your-app.example.com/health/nats/jetstream - Expected: HTTP 200, body contains
"status":"ok" - Interval: 1 minute
- Alert channel: Slack (P2 — stream degraded, not server down)
What These Monitors Catch
| Failure | Process Monitor | Vigilmon | |---|---|---| | NATS server crash | ✓ | ✓ | | NATS monitoring port unreachable | ✗ | ✓ | | JetStream consumer lag spike | ✗ | ✓ | | Network partition to NATS cluster | ✗ | ✓ | | Cluster leader election delay | ✗ | ✓ |
Step 3: Heartbeat Monitoring for NATS Subscribers
A NATS server can be fully healthy while your subscriber application is stuck in a reconnect loop, blocked on a downstream dependency, or silently missing messages due to a slow consumer error. The NATS /healthz endpoint won't surface any of this.
Vigilmon's heartbeat monitors prove subscriber liveness: your subscriber pings Vigilmon after each successfully processed message batch, and Vigilmon fires an alert if pings stop arriving.
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
nats-order-subscriber - Set the expected interval: 5 minutes (adjust to your message throughput)
- Set the grace period: 10 minutes
- Save — copy the heartbeat URL, e.g.
https://vigilmon.online/heartbeat/abc123xyz
Wire It Into Your Subscriber
Node.js (nats.js):
import { connect, StringCodec } from 'nats';
import axios from 'axios';
const sc = StringCodec();
async function run() {
const nc = await connect({
servers: process.env.NATS_URL || 'nats://localhost:4222',
});
const js = nc.jetstream();
const consumer = await js.consumers.get('events', 'order-processor');
let processedSinceLastHeartbeat = 0;
const HEARTBEAT_EVERY = 50;
const messages = await consumer.consume();
for await (const msg of messages) {
await processMessage(sc.decode(msg.data));
msg.ack();
processedSinceLastHeartbeat++;
if (processedSinceLastHeartbeat >= HEARTBEAT_EVERY) {
await axios.get(process.env.VIGILMON_HEARTBEAT_URL).catch(() => {});
processedSinceLastHeartbeat = 0;
}
}
}
run();
For low-throughput subscribers, use a timer-based heartbeat to prove the subscriber is alive even when no messages arrive:
// Heartbeat every 60 seconds if the subscriber is connected and consuming
setInterval(async () => {
if (nc && !nc.isClosed()) {
await axios.get(process.env.VIGILMON_HEARTBEAT_URL).catch(() => {});
}
}, 60_000);
Go:
package main
import (
"net/http"
"os"
"time"
"github.com/nats-io/nats.go"
)
func main() {
nc, _ := nats.Connect(os.Getenv("NATS_URL"))
js, _ := nc.JetStream()
// Timer heartbeat — proves subscriber is alive regardless of message volume
go func() {
ticker := time.NewTicker(60 * time.Second)
for range ticker.C {
if !nc.IsClosed() {
http.Get(os.Getenv("VIGILMON_HEARTBEAT_URL"))
}
}
}()
sub, _ := js.Subscribe("events.orders", func(msg *nats.Msg) {
processMessage(msg)
msg.Ack()
}, nats.Durable("order-processor"))
defer sub.Unsubscribe()
select {} // block forever
}
Step 4: Monitoring NATS Cluster Connectivity
For NATS clusters (JetStream-enabled), each server should have its own Vigilmon monitor:
# Monitor each cluster node's health endpoint independently
http://nats-node-1:8222/healthz
http://nats-node-2:8222/healthz
http://nats-node-3:8222/healthz
Use a naming convention that makes cluster membership obvious:
[nats-cluster] node-1 /healthz[nats-cluster] node-2 /healthz[nats-cluster] node-3 /healthz
Group them under a NATS Cluster Status Page. If two of three nodes fail simultaneously, your on-call team sees the full picture at a glance rather than receiving three independent alerts.
Step 5: Alert Routing for NATS Outages
Configure alert priorities to match the blast radius of each failure:
| Monitor | Alert Channel | Priority |
|---|---|---|
| NATS server /healthz | Slack + PagerDuty | P1 |
| JetStream /health/nats/jetstream | Slack | P2 |
| Subscriber heartbeat | Slack + email | P2 |
| Cluster node /healthz (individual) | Slack | P2 |
Response time thresholds to set:
- Alert at
500msfor/healthz— the NATS monitoring endpoint is always fast; slowness signals server pressure - Alert at
3000msfor JetStream checks — consumer info queries can take longer under high load
Summary
NATS failures are silent by design — publishers don't block, and dropped messages leave no visible trace. External monitoring through Vigilmon catches what internal tooling misses:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on nats-host:8222/healthz | NATS server liveness |
| HTTP monitor on JetStream health sidecar | Consumer lag, stream health |
| Heartbeat monitor | Subscriber application liveness |
| Per-node cluster monitors | Cluster degradation, leader election failures |
Get started free at vigilmon.online — your first NATS monitor is running in under two minutes.