Node.js
Two setups cover most Node.js services:
- A heartbeat monitor for scheduled jobs, queue workers and anything that runs without an inbound request.
- An HTTP monitor on a health endpoint for your public API or website.
You create monitors in the dashboard under Monitors → New monitor. The code below only sends pings or answers checks, so it needs no API key.
The examples use the global fetch from Node.js 18 or later and need no packages.
Heartbeat: report a job
Create a heartbeat monitor, copy its ping URL and set it as an environment variable. Keep it out of source control: anyone with the URL can send pings.
ASNIHER_HEARTBEAT_URL=https://app.asniher.com/api/heartbeat/<token>
Save this helper as asniher.mjs. It pings /start before the job, the success URL after it, and /fail with the error if it throws. A ping that cannot reach Asniher logs a warning and never fails your job.
const url = process.env.ASNIHER_HEARTBEAT_URL;
async function ping(suffix = "", body) {
if (!url) return;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
const res = await fetch(url + suffix, { method: "POST", body, signal: AbortSignal.timeout(10_000) });
if (!res.ok) console.warn(`Asniher ping returned ${res.status}`);
return;
} catch (err) {
if (attempt === 3) console.warn(`Asniher ping failed: ${err instanceof Error ? err.message : err}`);
}
}
}
export async function monitored(job) {
await ping("/start");
try {
const result = await job();
await ping();
return result;
} catch (err) {
const output = err instanceof Error && err.stack ? err.stack : String(err);
await ping("/fail", output.slice(-10_000));
throw err;
}
}
Wrap the job:
import { monitored } from "./asniher.mjs";
await monitored(async () => {
// your job, for example a nightly export
await exportOrders();
});
Run it from cron and copy the same cron expression into the monitor's schedule:
30 2 * * * cd /srv/app && ASNIHER_HEARTBEAT_URL=https://app.asniher.com/api/heartbeat/<token> node jobs/export-orders.mjs
The error stack is sent as job output, so it shows up in the ping log and the alert. Avoid putting secrets in error messages.
In-process schedulers
If the schedule lives inside your app (for example with node-cron), wrap the task the same way:
import cron from "node-cron";
import { monitored } from "./asniher.mjs";
cron.schedule("*/15 * * * *", () => monitored(syncInventory).catch(() => undefined));
Use the cron expression */15 * * * * on the monitor, with a grace period a little longer than the task's longest normal run.
Queue workers
A worker that loops forever has no "end of job". Ping on a fixed period while it is healthy instead. Set the monitor to that period and a grace period of one or two periods, so a crashed or stuck worker raises an alert.
import { setInterval } from "node:timers/promises";
for await (const _ of setInterval(60_000)) {
if (worker.isRunning() && !worker.isPaused()) {
await fetch(process.env.ASNIHER_HEARTBEAT_URL, { method: "POST", signal: AbortSignal.timeout(10_000) }).catch(() => undefined);
}
}
Replace the worker checks with whatever tells you the worker is processing, for example the time of the last completed job.
HTTP monitor: health endpoint
An HTTP monitor needs a public URL that returns 2xx when the service works and something else when it does not. Check the dependencies the service cannot work without, and return 503 if one is down.
Express
import express from "express";
import pg from "pg";
const app = express();
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
app.get("/healthz", async (_req, res) => {
res.set("cache-control", "no-store");
try {
await pool.query("select 1");
res.json({ status: "ok" });
} catch {
res.status(503).json({ status: "unavailable" });
}
});
app.listen(Number(process.env.PORT ?? 3000));
Next.js (App Router)
// app/healthz/route.ts
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
export const dynamic = "force-dynamic";
export async function GET() {
try {
await db.execute(sql`select 1`);
return Response.json({ status: "ok" }, { headers: { "cache-control": "no-store" } });
} catch {
return Response.json({ status: "unavailable" }, { status: 503, headers: { "cache-control": "no-store" } });
}
}
Swap the Drizzle query for your own database client. Then create an HTTP monitor:
| Setting | Value |
|---|---|
| URL | https://api.example.com/healthz |
| Method | GET |
| Expected status | 200 |
| Interval | Your plan's minimum |
Keep the endpoint fast and free of secrets. It runs on every check, from several regions when a failure is confirmed. Asniher identifies itself as AsniherBot/1.0; allow that user agent if bot protection sits in front of the endpoint.
Check that it works
curl -fsS -X POST "$ASNIHER_HEARTBEAT_URL"
The response is {"ok":true} and the ping appears in the monitor's ping log within seconds. A 404 means the token is wrong or the monitor was deleted.