Setting Up Uptime Monitoring for Fastify with Vigilmon
Fastify is one of the fastest Node.js web frameworks available. But raw throughput does not protect you from the failure modes that matter in production: a database pool that exhausts under load, a plugin that throws during initialization, or a background task that quietly dies.
This tutorial covers production uptime monitoring for Fastify applications using Vigilmon. We will walk through:
- A
/healthroute using Fastify plugins - Vigilmon multi-region HTTP monitoring setup
- Webhook alerts for DOWN/UP transitions
- Heartbeat monitoring for background workers
Prerequisites
- Node.js 18+
- An existing Fastify application (
fastifyv4+) - A free account at vigilmon.online
Part 1: Add a health check route
Fastify's plugin system is the idiomatic way to register routes with their own scope. A health plugin keeps your monitoring logic isolated.
Basic health plugin
// plugins/health.js
'use strict';
const fp = require('fastify-plugin');
async function healthPlugin(fastify) {
fastify.route({
method: 'GET',
url: '/health',
schema: {
response: {
200: {
type: 'object',
properties: {
status: { type: 'string' },
timestamp: { type: 'string' },
uptime: { type: 'number' },
checks: { type: 'object' },
},
},
503: {
type: 'object',
properties: {
status: { type: 'string' },
timestamp: { type: 'string' },
uptime: { type: 'number' },
checks: { type: 'object' },
},
},
},
},
// Skip authentication — monitoring must reach this without credentials
config: { skipAuth: true },
handler: async (request, reply) => {
const checks = {};
let status = 'ok';
// Database check
if (fastify.db) {
try {
await fastify.db.query('SELECT 1');
checks.database = 'ok';
} catch (err) {
checks.database = `error: ${err.message}`;
status = 'degraded';
}
}
// Redis check
if (fastify.redis) {
try {
await fastify.redis.ping();
checks.redis = 'ok';
} catch (err) {
checks.redis = `error: ${err.message}`;
status = 'degraded';
}
}
const code = status === 'ok' ? 200 : 503;
return reply.code(code).send({
status,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks,
});
},
});
}
// fp() wraps the plugin so it shares the parent scope's decorations (db, redis, etc.)
module.exports = fp(healthPlugin, {
name: 'health',
fastify: '4.x',
});
Register in your main app
// app.js
'use strict';
const Fastify = require('fastify');
const healthPlugin = require('./plugins/health');
const dbPlugin = require('./plugins/db');
const redisPlugin = require('./plugins/redis');
const buildApp = async () => {
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL ?? 'info',
},
});
// Register infrastructure first so health plugin sees the decorations
await app.register(dbPlugin);
await app.register(redisPlugin);
// Register health plugin — fp() means it joins the root scope
await app.register(healthPlugin);
return app;
};
module.exports = buildApp;
// server.js
const buildApp = require('./app');
const start = async () => {
const app = await buildApp();
await app.listen({ port: process.env.PORT ?? 3000, host: '0.0.0.0' });
};
start().catch(err => {
console.error(err);
process.exit(1);
});
Test it:
curl http://localhost:3000/health
{
"status": "ok",
"timestamp": "2026-06-29T08:00:00.000Z",
"uptime": 88.2,
"checks": {
"database": "ok",
"redis": "ok"
}
}
When a dependency is unreachable the handler returns HTTP 503. Vigilmon's HTTP monitor treats any non-2xx response as a failure and fires an alert.
TypeScript version
// plugins/health.ts
import fp from 'fastify-plugin'
import type { FastifyInstance, FastifyPluginOptions } from 'fastify'
async function healthPlugin(fastify: FastifyInstance, _opts: FastifyPluginOptions) {
fastify.route({
method: 'GET',
url: '/health',
handler: async (_request, reply) => {
const checks: Record<string, string> = {}
let status = 'ok'
try {
await (fastify as any).db.query('SELECT 1')
checks.database = 'ok'
} catch (err: unknown) {
checks.database = `error: ${(err as Error).message}`
status = 'degraded'
}
const code = status === 'ok' ? 200 : 503
return reply.code(code).send({
status,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks,
})
},
})
}
export default fp(healthPlugin)
Part 2: Set up multi-region HTTP monitoring in Vigilmon
- Log in to vigilmon.online and click Add Monitor.
- Choose HTTP(S) monitor.
- Enter your health URL:
https://yourapi.example.com/health - Set the check interval. The free tier supports 5-minute intervals; paid plans support 1-minute intervals.
- Add an alert channel: email, Slack, or a custom webhook URL.
- Click Save.
Vigilmon runs checks from multiple geographic regions simultaneously. It only fires an alert after a majority of regions agree the endpoint is down — this is the key differentiator from single-location checkers, which generate false positives on regional network hiccups.
Configure response assertions
In advanced monitor settings, add a keyword check on the response body:
- Expected keyword:
"status":"ok"(exact substring match)
This catches degraded responses that still return HTTP 200. If your health handler returns {"status":"degraded"} with a 200, Vigilmon will still alert because the keyword is missing.
Part 3: Webhook alerts
// plugins/webhooks.js
'use strict';
const fp = require('fastify-plugin');
async function webhooksPlugin(fastify) {
fastify.route({
method: 'POST',
url: '/webhooks/vigilmon',
config: { skipAuth: true },
schema: {
body: {
type: 'object',
required: ['monitor_name', 'status', 'url', 'checked_at'],
properties: {
monitor_id: { type: 'string' },
monitor_name: { type: 'string' },
status: { type: 'string', enum: ['up', 'down'] },
url: { type: 'string' },
response_code: { type: 'integer' },
response_time_ms: { type: 'number' },
checked_at: { type: 'string' },
},
},
},
handler: async (request, reply) => {
const { monitor_name, status, url, response_code, checked_at } = request.body;
if (status === 'down') {
fastify.log.error({ monitor_name, url, response_code, checked_at }, 'Vigilmon: DOWN');
await notifyOnCall({ monitor_name, url, response_code });
} else {
fastify.log.info({ monitor_name }, 'Vigilmon: recovered');
}
return reply.code(204).send();
},
});
}
async function notifyOnCall({ monitor_name, url, response_code }) {
const slackUrl = process.env.SLACK_WEBHOOK_URL;
if (!slackUrl) return;
await fetch(slackUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `*ALERT*: ${monitor_name} is DOWN\nURL: ${url}\nHTTP: ${response_code ?? 'timeout'}`,
}),
});
}
module.exports = fp(webhooksPlugin, { name: 'webhooks' });
Register it in your app:
await app.register(require('./plugins/webhooks'));
In Vigilmon, navigate to your monitor → Alert channels → Add → Webhook, and enter https://yourapi.example.com/webhooks/vigilmon.
Vigilmon's webhook payload:
{
"monitor_id": "mon_abc123",
"monitor_name": "Fastify API /health",
"status": "down",
"url": "https://yourapi.example.com/health",
"checked_at": "2026-06-29T08:01:00Z",
"response_code": 503,
"response_time_ms": 1301
}
Part 4: Heartbeat monitoring for background workers
Fastify is often paired with a background job system (BullMQ, Bee-Queue, etc.). HTTP monitoring will not catch a worker process that is alive but no longer processing jobs.
The heartbeat pattern closes this gap: ping Vigilmon on each successful job cycle. Silence triggers an alert.
Create the heartbeat monitor in Vigilmon
- In Vigilmon, click Add Monitor → Heartbeat monitor.
- Set expected interval to match your worker's cycle (e.g., 5 minutes).
- Copy the unique ping URL.
- Set
VIGILMON_HEARTBEAT_URLin your production environment.
Worker with heartbeat (BullMQ)
// workers/emailWorker.js
'use strict';
const { Worker } = require('bullmq');
const Redis = require('ioredis');
const connection = new Redis(process.env.REDIS_URL);
const HEARTBEAT_URL = process.env.VIGILMON_HEARTBEAT_URL;
const worker = new Worker(
'email',
async (job) => {
await sendEmail(job.data);
},
{ connection, concurrency: 5 }
);
let lastHeartbeat = 0;
const HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000;
worker.on('completed', async (job) => {
const now = Date.now();
if (HEARTBEAT_URL && now - lastHeartbeat >= HEARTBEAT_INTERVAL_MS) {
try {
await fetch(HEARTBEAT_URL, { signal: AbortSignal.timeout(10_000) });
lastHeartbeat = now;
} catch (err) {
console.warn('Heartbeat ping failed:', err.message);
}
}
});
worker.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err);
// No heartbeat ping on failure
});
console.log('Email worker started');
Standalone heartbeat loop
// workers/scheduler.js
'use strict';
const HEARTBEAT_URL = process.env.VIGILMON_HEARTBEAT_URL;
const INTERVAL_MS = 5 * 60 * 1000;
async function pingHeartbeat() {
if (!HEARTBEAT_URL) return;
try {
const res = await fetch(HEARTBEAT_URL, { signal: AbortSignal.timeout(10_000) });
if (!res.ok) console.warn(`Heartbeat returned ${res.status}`);
} catch (err) {
console.warn('Heartbeat failed:', err.message);
}
}
async function tick() {
try {
await runScheduledTasks();
await pingHeartbeat(); // only on success
} catch (err) {
console.error('Scheduler error:', err);
}
}
tick();
setInterval(tick, INTERVAL_MS);
Part 5: Fastify-specific tips
Graceful shutdown
Fastify's fastify.close() hooks let you drain active connections before the process exits. Add a SIGTERM handler so your health route returns 503 during rolling deploys, which prevents Vigilmon from alerting on intentional restarts:
let shuttingDown = false;
process.on('SIGTERM', async () => {
shuttingDown = true;
await app.close();
process.exit(0);
});
// In health handler
if (shuttingDown) {
return reply.code(503).send({ status: 'shutting_down' });
}
Structured logging with pino
Fastify's built-in pino logger emits structured JSON. Log response time in your health handler so you can correlate Vigilmon alert timestamps with your log aggregator:
handler: async (request, reply) => {
const start = Date.now();
// ... dependency checks ...
const durationMs = Date.now() - start;
fastify.log.info({ durationMs, status }, 'health check');
return reply.code(code).send({ status, durationMs, checks });
}
Summary
Your Fastify application now has production-grade monitoring across multiple layers:
- Health plugin — checks real dependencies with Fastify's plugin scoping; returns HTTP 503 so Vigilmon alerts automatically.
- Webhook plugin — receives Vigilmon DOWN/UP events with Fastify's JSON schema validation and routes them to Slack or any on-call tool.
- Worker heartbeat — pings Vigilmon after each successful job cycle; silence triggers an alert before users notice.
- Multi-region consensus — Vigilmon requires agreement across regions before alerting, eliminating single-node false positives.
- Graceful shutdown — health returns 503 during deploys so you do not get alert noise on intentional restarts.
Vigilmon's free tier covers up to 5 monitors with 5-minute check intervals — enough for your API, a worker, and a cron from day one.
Monitor your Fastify app free at vigilmon.online
Top comments (0)