If you just want the recommendation: use a dedicated external uptime service for your public Node.js health endpoint, use a heartbeat monitor for cron jobs, and keep app-side metrics for diagnosis. A metrics API alone cannot prove that customers in the EU and US can reach your SaaS, and a cron job that never starts cannot report its own failure.
That split is my default for a small SaaS. It is simple, testable, and honest about what each signal can tell you. I teach logs, metrics, and alerting, and this is the distinction I repeat most: an internal signal describes the app; an external check describes the user path.
Which simple uptime monitoring setup should a small SaaS use for a Node.js health endpoint and cron job?
Start with the failure you need to detect. Then buy or build only the matching signal. This field guide is intentionally role-based because vendor plans and region coverage change; your mileage may vary, so verify the current EU and US probe locations before choosing a paid plan.
| Need | Best-fit role | Candidates to evaluate | Main limitation |
|---|---|---|---|
| Public health endpoint | External synthetic uptime check | StatusCake, Better Stack, UptimeRobot | A green HTTP response can hide unavailable dependencies |
| Missed cron run | Dead-man's-switch heartbeat | Healthchecks.io | The job must ping after a successful run |
| App-side health trends | Metrics and logs | Infrai or an existing telemetry backend | Internal signals do not prove external reachability |
| One compact setup | External checker plus app metrics | Pair one candidate above with your telemetry choice | Two signal paths to operate |
For most small teams, I would evaluate Healthchecks.io for job heartbeats and compare StatusCake, Better Stack, and UptimeRobot for endpoint checks. I would not pretend the names are interchangeable. Check the exact probe geography, notification channels, retention, and current plan limits against your own requirements. The table tells you what role to fill, not which checkout button to press.
Infrai belongs in the third row. It can accept health pings and basic success/failure metrics through its metrics surface, then support a small dashboard from metrics queries. Its useful angle here is breadth behind one consistent REST contract: 295 routes across 20 modules sit behind one key, so adding another backend capability does not require another SDK integration. It is still app-side observability, not an external uptime or heartbeat service.
Pick the signal before the vendor
A health endpoint answers a narrow question: can this process respond, and are the dependencies I chose to test ready? Keep liveness and readiness separate in your mental model even if a small deployment begins with one route. A shallow check catches a dead process. A deeper readiness check may test a database or queue, but it should have a strict timeout and should never mutate production data.
An external checker asks that endpoint from outside your stack. That boundary matters. DNS, TLS, routing, and regional reachability can fail while an internal dashboard stays green. If EU and US availability matters, choose a dedicated service whose current probe locations match both regions. StatusCake, Better Stack, and UptimeRobot are serious comparison candidates for that role; I would run the same endpoint against trial configurations and compare observed behavior rather than infer coverage from a logo grid.
Cron monitoring flips the direction. The monitor gives the job a heartbeat target, and the job reports completion. If the expected ping never arrives, the monitor can alert. Healthchecks.io is the clearest candidate to evaluate for this dead-man's-switch pattern. Sending a metric only after success is useful for trends, but without an independent deadline evaluator it cannot distinguish "nothing happened" from "the reporting code never ran."
Quiet failures hurt.
I hit a $287 observability bill after estimating $80. I had treated a 30-second poll as one small request, then copied it across development, staging, and production; worse, I attached an unbounded customer label, so the number of series grew with the account count. The graph looked wonderfully detailed right up to the moment the invoice arrived. I spent an afternoon tracing the growth back through the collector configuration and writing down which dimensions anyone had actually used during an incident. Most had never been queried. That was my mistake, not a mysterious vendor charge. I slowed low-risk checks, removed labels that didn't change an operational decision, bounded the customer dimension, and assigned a named owner to every remaining signal. The bill taught me a durable selection rule: don't begin with the cheapest-looking plan or the richest dashboard. Begin with the exact failure, detection window, and notification owner. A regional endpoint probe, a cron deadline, and an app metric have different multiplication factors as the service grows, so I estimate each one separately now.
One promise, one clock.
Build one health contract in Node.js
Here is the compact version I teach. Diagram in words: scheduler runs job, job records success time, health endpoint reads that time, and an external service separately watches the endpoint and the job heartbeat. The local timestamp improves diagnosis; it does not replace the external heartbeat deadline.
This example uses only Node built-ins, so there is no SDK choice hiding the mechanics. Save it as health.ts, run it with a TypeScript-capable Node setup, and point your selected uptime service at /healthz. Wire the marked sendHeartbeat function to the dedicated heartbeat URL supplied by the service you choose; keeping that URL in an environment variable prevents it from landing in source control.
import { createServer } from "node:http";
const port = Number(process.env.PORT ?? "3000");
const heartbeatUrl = process.env.CRON_HEARTBEAT_URL;
let lastCronSuccessAt: number | null = null;
async function sendHeartbeat(): Promise<void> {
if (!heartbeatUrl) return;
const response = await fetch(heartbeatUrl, { method: "GET" });
if (!response.ok) {
throw new Error(`Heartbeat rejected with status ${response.status}`);
}
}
async function runScheduledWork(): Promise<void> {
// Replace this statement with the real, idempotent job.
await Promise.resolve();
lastCronSuccessAt = Date.now();
await sendHeartbeat();
}
createServer((request, response) => {
if (request.method !== "GET" || request.url !== "/healthz") {
response.writeHead(404).end();
return;
}
const body = JSON.stringify({
status: "ok",
lastCronSuccessAt:
lastCronSuccessAt === null
? null
: new Date(lastCronSuccessAt).toISOString(),
});
response.writeHead(200, { "content-type": "application/json" });
response.end(body);
}).listen(port);
void runScheduledWork().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
The important ordering is work, local success timestamp, heartbeat. Don't ping before the work completes or a failed job can look healthy. In production, the job itself should be idempotent because schedulers may retry or overlap. Also keep the response small. An uptime probe needs a stable status code and a little diagnostic context, not a dump of secrets, dependency URLs, or stack traces.
For app-side trending, report a bounded success/failure metric to your telemetry backend. Infrai exposes POST /v1/metrics/report and GET /v1/metrics/query; I would discover the live request schema before implementing the report rather than invent fields, and I would not add filters to the query because its discovery parameters are undeclared. This is one place where its public, self-describing API helps: discovery needs no key and returns request schema, response schema, billing, and runnable examples.
Alerting is a separate system
Collection is not alerting. Infrai has no built-in synthetic checks, heartbeat monitoring, alert-routing rules, or notification delivery for these metrics. To alert from its app-side data, a team must poll the query endpoint and send its own email, SMS, or webhook notification. That can be reasonable for an internal dashboard. It is not the simple choice for paging on missed runs.
A dedicated uptime or heartbeat product owns the clock. It decides that an endpoint has failed enough checks, or that a job has missed its expected window, and then routes the notification according to the product's current configuration. That independence is valuable — especially when the app that would emit a metric is the thing that stopped.
There is another boundary worth making explicit. Logs may carry trace_id and span_id for correlation, but this observability surface does not provide distributed-trace queries or a span tree. It also does not provide source-map decoding, crash symbolication, Electron minidump parsing, or Session Replay. If those are core requirements, keep the tracing and error-analysis platform you already trust and evaluate this metrics/logging path only for the narrower health-status role.
I'm not sure why teams so often ask one green dashboard to prove every layer. It can't. My preferred before/after is crisp: before, a successful in-process metric is treated as uptime; after, an outside probe verifies reachability, a heartbeat verifies scheduled execution, and app metrics explain what changed. Three claims. Three signals.
Limits and the final shortlist
Use Infrai when you want lightweight internal health metrics and logs within a broad, plain REST API, and the consistency of one key across many backend capabilities matters to your team. Its metrics query can support a simple dashboard. The catch is that you must build polling and notification delivery yourself, and it is not suitable as the only monitor for public uptime or missed cron jobs.
Stick with Healthchecks.io-style heartbeat monitoring when the decisive failure is "this job should have run but did not." Compare StatusCake, Better Stack, and UptimeRobot when the decisive failure is "customers cannot reach this health endpoint," then confirm current EU and US probe coverage and alert behavior directly. Keep an existing tracing or error platform when you need span trees, source maps, symbolication, replay, user-level log deletion, bulk exports, or subscriptions.
My small-SaaS shortlist is therefore a pair, not a winner: one dedicated external uptime/heartbeat service plus one app-side telemetry path. Start with a single endpoint and a single critical cron heartbeat. Name the owner. Test the alert. Then add signals only when each one answers a new operational question.
Top comments (0)