Node.js integration
Wire Drumbeats into a Node.js or TypeScript codebase. Quick example, centralized setup, error handling, and production patterns.
This guide shows how to wire Drumbeats into a Node.js or TypeScript project. By the end you have a single helper that wraps any job with start / success / failure pings and surfaces errors to the team without any per-job boilerplate.
The patterns below assume Node 18+ for built-in fetch. They work the same on Bun and Deno; replace fetch with the platform equivalent if you are on Node 16.
Quick example
The minimal shape — one ping at the start, one at the end:
const API = "https://api.drumbeats.io/v1/ping/<monitor-id>";
await fetch(`${API}/start`);
try {
await runMyJob();
await fetch(`${API}/success`);
} catch (err) {
await fetch(`${API}/failure`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload: String(err) }),
});
throw err;
}const API = "https://api.drumbeats.io/v1/ping/<monitor-id>";
await fetch(`${API}/start`);
try {
await runMyJob();
await fetch(`${API}/success`);
} catch (err) {
await fetch(`${API}/failure`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload: String(err) }),
});
throw err;
}POST the failure with a payload so the error message shows up on the incident timeline. The success ping can stay as a GET — it does not need a body.
Centralized setup
For apps with more than two monitored jobs, hoist the monitor IDs and the wrapper into a single module so adding a monitor never requires copy-pasting glue code.
const BASE = process.env.DRUMBEATS_BASE_URL ?? "https://api.drumbeats.io/v1";
export const MONITORS = {
dailyBackup: "11111111-2222-3333-4444-555555555555",
hourlySync: "66666666-7777-8888-9999-aaaaaaaaaaaa",
newsletterSend: "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
} as const;
export type MonitorKey = keyof typeof MONITORS;
async function ping(monitor: string, event: "start" | "success" | "failure" | "log", runId?: string, payload?: unknown) {
const url = new URL(`${BASE}/ping/${monitor}/${event}`);
if (runId) url.searchParams.set("run_id", runId);
if (payload === undefined) {
await fetch(url.toString());
return;
}
await fetch(url.toString(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload: typeof payload === "string" ? payload : JSON.stringify(payload) }),
});
}
export async function withMonitor<T>(
key: MonitorKey,
fn: () => Promise<T>,
options?: { runId?: string },
): Promise<T> {
const monitorId = MONITORS[key];
const runId = options?.runId ?? `${key}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
await ping(monitorId, "start", runId);
try {
const result = await fn();
await ping(monitorId, "success", runId);
return result;
} catch (err) {
await ping(monitorId, "failure", runId, err instanceof Error ? err.stack ?? err.message : String(err));
throw err;
}
}const BASE = process.env.DRUMBEATS_BASE_URL ?? "https://api.drumbeats.io/v1";
export const MONITORS = {
dailyBackup: "11111111-2222-3333-4444-555555555555",
hourlySync: "66666666-7777-8888-9999-aaaaaaaaaaaa",
newsletterSend: "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
} as const;
export type MonitorKey = keyof typeof MONITORS;
async function ping(monitor: string, event: "start" | "success" | "failure" | "log", runId?: string, payload?: unknown) {
const url = new URL(`${BASE}/ping/${monitor}/${event}`);
if (runId) url.searchParams.set("run_id", runId);
if (payload === undefined) {
await fetch(url.toString());
return;
}
await fetch(url.toString(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload: typeof payload === "string" ? payload : JSON.stringify(payload) }),
});
}
export async function withMonitor<T>(
key: MonitorKey,
fn: () => Promise<T>,
options?: { runId?: string },
): Promise<T> {
const monitorId = MONITORS[key];
const runId = options?.runId ?? `${key}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
await ping(monitorId, "start", runId);
try {
const result = await fn();
await ping(monitorId, "success", runId);
return result;
} catch (err) {
await ping(monitorId, "failure", runId, err instanceof Error ? err.stack ?? err.message : String(err));
throw err;
}
}Now every job is a one-liner:
import { withMonitor } from "../monitoring/drumbeats";
export async function runDailyBackup() {
return withMonitor("dailyBackup", async () => {
await dumpDatabase();
await uploadToS3();
});
}import { withMonitor } from "../monitoring/drumbeats";
export async function runDailyBackup() {
return withMonitor("dailyBackup", async () => {
await dumpDatabase();
await uploadToS3();
});
}Add a new monitor: update MONITORS, wrap the job in withMonitor. That is the entire change.
Error handling
The wrapper above only catches errors that runMyJob() rethrows. A few cases need explicit handling:
- Synchronous throws in a
setInterval/setTimeoutcallback. Wrap the callback body inwithMonitorinstead of the scheduler call. - Unhandled promise rejections. If you rely on
process.on('unhandledRejection'), send a failure ping there too so detached promises still alert. - Process-level exits. A worker that
process.exit(1)s before the failure ping returns will lose the alert. Use the slug-based ping withawaitso the network request completes before exit, or send a finalstartonly after the process is alive enough to send the matchingfailure.
For workers handling messages (queue consumers), each message gets its own withMonitor call with a unique run_id:
queue.consume(async (message) => {
await withMonitor("messageProcessor", () => processMessage(message), {
runId: `msg-${message.id}`,
});
});queue.consume(async (message) => {
await withMonitor("messageProcessor", () => processMessage(message), {
runId: `msg-${message.id}`,
});
});Production patterns
A small set of patterns turn the basic wrapper into a production-grade integration.
Time-bound the ping itself
A slow or hung Drumbeats request should not block your job. Wrap the fetch in AbortSignal.timeout:
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3_000);
await fetch(url, { signal: controller.signal }).catch(() => {});
clearTimeout(timer);const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3_000);
await fetch(url, { signal: controller.signal }).catch(() => {});
clearTimeout(timer);A failed ping should never throw — your job is the priority, not the side-channel.
Cap payload size
Drumbeats stores up to 25KB per ping payload for free; anything beyond costs additional beats (see Beats and usage). Truncate long stack traces before sending:
const MAX_PAYLOAD = 20_000;
const payload = err instanceof Error ? (err.stack ?? err.message) : String(err);
const truncated = payload.length > MAX_PAYLOAD ? `${payload.slice(0, MAX_PAYLOAD)}…` : payload;const MAX_PAYLOAD = 20_000;
const payload = err instanceof Error ? (err.stack ?? err.message) : String(err);
const truncated = payload.length > MAX_PAYLOAD ? `${payload.slice(0, MAX_PAYLOAD)}…` : payload;Send progress logs
For jobs that take more than a few seconds, send log pings so the incident timeline shows progress instead of a single start/finish pair:
await ping(MONITORS.dailyBackup, "log", runId, "Compressed dump (45MB)");await ping(MONITORS.dailyBackup, "log", runId, "Compressed dump (45MB)");log pings count as beats but are the cheapest observability you can add to a long-running job.
Use the slug endpoint for multi-environment
If you run the same job in staging and production, the slug-based endpoint avoids per-env monitor-ID juggling:
const url = `${BASE}/s-ping/${PROJECT_ID}/${SLUG}/${event}`;const url = `${BASE}/s-ping/${PROJECT_ID}/${SLUG}/${event}`;Set SLUG = "daily-backup" in both environments; Drumbeats routes to the right monitor based on the project ID. See Ping API: scheduled pings for the parameter list.
Related guides
- Monitor types — pick Cron, Heartbeat, or Event-driven before wiring pings.
- Ping API — every endpoint, query parameter, and event type.
- Production hardening — retries, timeouts, observability patterns for any language.
- Alternatives — how Drumbeats compares to Cronitor and Healthchecks.io when you're picking a vendor.