tutorial

How to Monitor NATS Server Health and JetStream with Vigilmon

NATS is the cloud-native messaging backbone — but server partitions, JetStream stream stalls, and cluster split-brains can silently halt message delivery. Learn how to monitor NATS with Vigilmon HTTP probes and heartbeat monitors.

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

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL to: http://nats-host:8222/healthz
  4. Set the check interval to 1 minute
  5. Under Expected response, configure:
    • Status code: 200
    • Response body contains: "status":"ok"
    • Response time threshold: 2000ms
  6. Under Alert channels, assign your Slack or PagerDuty channel
  7. 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

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: nats-order-subscriber
  3. Set the expected interval: 5 minutes (adjust to your message throughput)
  4. Set the grace period: 10 minutes
  5. 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 500ms for /healthz — the NATS monitoring endpoint is always fast; slowness signals server pressure
  • Alert at 3000ms for 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.

Monitor your app with Vigilmon

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

Start free →