Skip to content

Heartbeat plugin

@warden/heartbeat adds health monitoring to your Warden bot by sending periodic pings to an external status service. If the pings stop, the service knows something is wrong and can alert you. It’s a simple, effective way to know your bot is actually running — because finding out your bot is down when users need it most is not the experience you want.

The plugin works with popular uptime monitoring services like Uptime Kuma, Betterstack (formerly Better Uptime), and Healthchecks.io. If your service accepts an HTTP ping, it works with this plugin.

Terminal window
pnpm add @warden/heartbeat

The peer dependencies are:

PackageDescription
@warden/coreWarden framework (you already have this)

No additional drivers or adapters needed — the plugin makes plain HTTP requests.

Register HeartbeatServiceProvider in your bootstrap file:

import {Bot} from '@warden/core';
import {HeartbeatServiceProvider} from '@warden/heartbeat';
new Bot()
.discover(d => d
.interactions('./src/{commands,buttons,modals,menus}/**/*.ts')
.events('./src/events/**/*.ts'))
.intents(i => i.defaults())
.partials(p => p.defaults())
.plugins([HeartbeatServiceProvider])
.start();

Then configure the heartbeat URL and interval:

src/config/heartbeat.ts
import {env} from '@warden/core';
export default {
url: env('HEARTBEAT_URL'),
interval: env.number('HEARTBEAT_INTERVAL', 60),
};

And set the environment variables:

.env
HEARTBEAT_URL=https://uptime.example.com/api/push/your-monitor-id
HEARTBEAT_INTERVAL=60

That’s the entire setup. The plugin starts pinging during boot() and stops cleanly during shutdown().

The heartbeat is straightforward: at a fixed interval, the plugin sends an HTTP GET request to your configured URL. If the request succeeds, your monitoring service records that the bot is alive. If the request stops arriving (because the bot crashed, the server went down, or the process was killed), the monitoring service notices the gap and triggers an alert.

Bot running → ping → ping → ping → ping → ...
Bot crashes → ping → ping → (silence)
Monitoring service → ⚠ alert triggered

The interval is configurable, but 60 seconds is a sensible default. Most monitoring services expect pings within a grace period (typically 2-3x your interval), so a 60-second interval gives you a 2-3 minute detection window.

The heartbeat plugin works with any service that accepts an HTTP ping at a URL. Here are the most popular ones and how to set them up:

Uptime Kuma is a self-hosted monitoring tool. Create a new monitor of type “Push” and use the provided URL:

HEARTBEAT_URL=https://your-kuma-instance.com/api/push/abc123?status=up&msg=OK

By default, the heartbeat plugin just pings the URL on a timer. But sometimes you want to verify that the bot is actually healthy before reporting that it’s alive. Maybe the database connection dropped, or Redis is unreachable, or the Discord gateway is disconnected.

Other plugins (and your own code) can register health checks that are evaluated before each ping. If any health check fails, the ping is skipped — which causes your monitoring service to trigger an alert:

import {Bot, Plugin} from '@warden/core';
import {HeartbeatService} from '@warden/heartbeat';
export class DatabaseHealthPlugin implements Plugin {
register(bot: Bot): void {
// ...
}
boot(bot: Bot): void {
const heartbeat = bot.resolve(HeartbeatService);
const db = bot.resolve(DrizzleService);
heartbeat.addCheck('database', async () => {
// Run a lightweight query to verify connectivity
await db.query.execute('SELECT 1');
return true;
});
}
}

You can register as many health checks as you like. Each check is a named async function that returns true for healthy or false (or throws) for unhealthy. All checks must pass for the ping to be sent:

// In your bootstrap or a plugin's boot() method
const heartbeat = bot.resolve(HeartbeatService);
heartbeat.addCheck('database', async () => {
await db.query.execute('SELECT 1');
return true;
});
heartbeat.addCheck('cache', async () => {
await cache.set('health:check', 'ok', 5);
return await cache.get('health:check') === 'ok';
});
heartbeat.addCheck('discord', () => {
return bot.client.ws.status === 0; // 0 = READY
});

If a health check throws an error, the plugin catches it and treats it as a failure. The error is logged so you can investigate, but the bot keeps running — it just stops reporting as healthy until the issue resolves.

The heartbeat plugin reads from the heartbeat config namespace:

src/config/heartbeat.ts
import {env} from '@warden/core';
export default {
url: env('HEARTBEAT_URL'),
interval: env.number('HEARTBEAT_INTERVAL', 60), // seconds between pings
};
OptionTypeDefaultDescription
urlstringThe URL to ping. If not set, the plugin is disabled.
intervalnumber60Seconds between pings.

Most monitoring services let you configure a “grace period” — how long to wait after a missed ping before alerting. Set this to 2-3x your heartbeat interval. With a 60-second interval, a 2-3 minute grace period prevents false alarms from brief network blips.

In development, a simple timer-based ping is fine. In production, always register health checks for your critical dependencies. A bot that’s running but can’t reach the database isn’t really “up” from a user’s perspective:

// A good production setup
heartbeat.addCheck('database', async () => {
await db.query.execute('SELECT 1');
return true;
});
heartbeat.addCheck('discord', () => {
return bot.client.ws.status === 0;
});

Don’t forget the heartbeat URL in production deploys

Section titled “Don’t forget the heartbeat URL in production deploys”

A common gotcha: you set everything up locally, deploy to production, and forget to set HEARTBEAT_URL in the production environment. The plugin silently does nothing (by design), and you think you have monitoring when you don’t. Add it to your deployment checklist or use env.required() if you want a hard failure:

// src/config/heartbeat.ts — strict version
import {env} from '@warden/core';
export default {
url: env.required('HEARTBEAT_URL'), // bot won't start without this
interval: env.number('HEARTBEAT_INTERVAL', 60),
};