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'); // your database plugin
const redisPlugin = require('./plugins/redis'); // your Redis plugin
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'}`,
}),
}).catch(err => fastify.log.warn({ err }, 'Slack notify failed'));
}
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
// 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) => {
// Process the job
await sendEmail(job.data);
},
{
connection,
concurrency: 5,
}
);
let lastHeartbeat = 0;
const HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000;
worker.on('completed', async (job) => {
// Throttle heartbeat pings — send at most once per interval
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 — Vigilmon alerts on silence
});
console.log('Email worker started');
Standalone heartbeat loop (without BullMQ)
// 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(); // run immediately on start
setInterval(tick, INTERVAL_MS);
Part 5: Fastify-specific tips
Use fastify-healthcheck for simple cases
If you only need a basic liveness probe without dependency checks:
npm install fastify-healthcheck
await app.register(require('fastify-healthcheck'));
// Registers GET /health automatically
This works for Kubernetes liveness probes but does not check your dependencies. The custom plugin from Part 1 is better for Vigilmon because it returns HTTP 503 on dependency failure, which triggers an alert.
Structured logging integrates with Vigilmon response time
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 });
}
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' });
}
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
#fastify #javascript #monitoring #devops