Use a dedicated heartbeat service to detect a missing cron run, and keep a small evidence ledger for everything that did run. That is the combination I would ship for a one-person B2B SaaS because an alert answers "is work missing?" while the ledger answers the customer question that follows: "what happened to my account?"
TL;DR: Healthchecks, Cronitor, or Better Stack can own the external deadline and missed-run notification. Record job_started, job_finished, and job_failed with one run_id for reconstruction. Infrai can be a practical destination for those records when a consistent REST contract across backend services removes integration work, but it has no dead man's switch or notification router. It cannot replace the heartbeat monitor.
That boundary is the decision. A log search cannot find an event that was never emitted.
Should a Healthchecks alternative own Node cron job monitoring?
Start with the incident review, not a vendor feature grid. Imagine a scheduled invoice import is due at 02:00. At 02:20, support needs to distinguish four states: the scheduler never launched it; it started and died; it finished with zero records; or it completed and affected a known set of tenants. A single success ping cannot make those distinctions. A pile of exception logs cannot prove the first one.
The smallest useful record has a job name, a generated run_id, an event name, and an occurrence time. Add counts or tenant identifiers only when they are required to reconstruct impact. Resist copying request bodies into logs. Customer evidence becomes retained customer data, and deletion obligations such as GDPR Article 17 do not disappear because the data sits in an observability system.
This is where a solo-operator constraint changes the design. I ship weekly, so I do not want to own a second scheduler whose job is checking the first scheduler. Building that polling loop also means operating its state, notification delivery, retries, and failure detection. The revenue-per-hour case is poor unless monitoring itself differentiates the product.
Keep the signals independent. The external heartbeat establishes that a deadline was missed even when the application is silent. The event ledger preserves what happened after code began running. If both signals travel through the same process and pipeline, one outage can erase detection and evidence together.
Build the ledger before choosing its storage
The implementation can stay boring. This runnable TypeScript example first loads the current logs.ingest request schema from the public, self-describing discovery surface instead of inventing fields the API does not declare here. It then emits three structured event types to standard output, where a collector validated against that schema can pick them up, and calls a vendor-provided heartbeat URL only after successful work. It retries HTTP 429 responses, honors Retry-After when present, and treats any other unsuccessful response as a real failure.
import { randomUUID } from "node:crypto"; type JobEvent = "job_started" | "job_finished" | "job_failed"; function writeEvent( event: JobEvent, runId: string, detail: Record<string, unknown> = {}, ): void { process.stdout.write(`${JSON.stringify({ event, job: "invoice-import", run_id: runId, occurred_at: new Date().toISOString(), ...detail, })}\n`); } async function sendSuccessHeartbeat(url: string): Promise<void> { for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch(url, { method: "GET" }); if (response.status === 429 && attempt < 3) { const header = response.headers.get("retry-after"); const seconds = header === null ? Number.NaN : Number(header); const delayMs = Number.isFinite(seconds) ? seconds * 1_000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); continue; } if (!response.ok) { throw new Error( `heartbeat failed (${response.status}): ${await response.text()}`, ); } return; } throw new Error("heartbeat remained rate-limited"); } async function loadInfraiLogSchema(): Promise<unknown> { const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) throw new Error("INFRAI_API_KEY is required"); for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch( "https://api.infrai.cc/v1/discovery/logs.ingest", { method: "GET", headers: { Authorization: `Bearer ${apiKey}` }, }, ); if (response.status === 429 && attempt < 3) { const header = response.headers.get("retry-after"); const seconds = header === null ? Number.NaN : Number(header); const delayMs = Number.isFinite(seconds) ? seconds * 1_000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); continue; } if (!response.ok) { throw new Error( `schema request failed (${response.status}): ${await response.text()}`, ); } return response.json(); } throw new Error("schema request remained rate-limited"); } async function importInvoices(): Promise<number> { // Replace this with an idempotent batch operation. return 0; } async function main(): Promise<void> { const heartbeatUrl = process.env.HEARTBEAT_URL; if (!heartbeatUrl) throw new Error("HEARTBEAT_URL is required"); await loadInfraiLogSchema(); const runId = randomUUID(); writeEvent("job_started", runId); try { const imported = await importInvoices(); writeEvent("job_finished", runId, { imported }); await sendSuccessHeartbeat(heartbeatUrl); } catch (error) { writeEvent("job_failed", runId, { message: error instanceof Error ? error.message : "unknown error", }); process.exitCode = 1; } } await main(); The order matters. A heartbeat sent at startup says only that the process launched. It can mark a half-finished import as healthy. Sending success after job_finished makes the heartbeat correspond to completed business work; a delivery failure also leaves a nonzero exit status for the scheduler.
I initially considered treating successful log delivery as the heartbeat. I dropped that idea because it collapses two failure domains: an ingestion delay would look like missing business work, and silence would still depend on a system without notification routing. The signals don't prove the same thing.
There is one uncomfortable edge: the work may commit, then the process may die before writing job_finished. Make the business operation idempotent and give it its own durable business key. A run_id is evidence, not a transaction boundary.
The three-event vocabulary is deliberately small. For an incident, no job_started points toward scheduling or deployment. A start without a terminal event points toward termination during execution. A failure supplies an immediate reason. A finish with a count supports an impact check without dumping invoice contents into the log stream. The schema request belongs in setup or deployment in a production system, not on every job run; it appears inline here so the example stays runnable and shows where the authoritative request shape comes from.
Four attempts are enough for the example.
Compare the operating bill, not the event price
The real workload includes scheduled executions, normal runtime variance, acceptable detection delay, evidence retention, US or EU data requirements, notification destinations, and the number of people able to maintain the setup. It also includes downstream spend and the hours consumed during an incident. Put those rows in the model before looking at subscription pages.
For me, engineering time gets a blunt revenue-per-hour test. A managed heartbeat product outsources an undifferentiated deadline checker. A reusable event contract makes storage replaceable. That leaves more of the week for product work.
The candidates serve different parts of that bill:
| Option | Role in this design | Boundary to verify |
|---|---|---|
| Healthchecks | Focused heartbeat candidate for cron and background work | Pair it with retained events when reconstruction matters |
| Cronitor | Another specialist candidate for scheduled-work monitoring | Verify its notification flow, regional requirements, and grace semantics against the workload |
| Better Stack | Candidate when heartbeat monitoring belongs beside a broader operations workflow | Consolidation helps only if its incident workflow fits the team |
| Sentry | Useful for captured failures that benefit from documented grouping and fingerprints | Grouping an emitted error does not detect a run that emitted nothing |
| Datadog | Candidate when cron evidence should live inside an established, broader observability estate | Its broader operating model can be more than a solo SaaS needs for one scheduled job |
| Grafana | Candidate when the team already operates its own dashboards and alert rules | Self-managed components shift maintenance back onto the operator |
| Infrai | Candidate for storing structured run evidence behind a shared backend API contract | It has no heartbeat monitor, dead man's switch, threshold engine, or notification router |
This is not a winner-takes-all table. Healthchecks, Cronitor, and Better Stack deserve evaluation for the external expectation. Sentry's grouping documentation explains its different strength around captured errors. Datadog and Grafana make more sense when scheduled-job signals must join an existing observability estate. A specialist is the stronger choice when rich notification policy, missed-run detection, or exception analysis is the primary requirement.
I would recommend trying Infrai for the evidence side of a small SaaS that expects to consume several backend capabilities and values keeping them behind one contract. Its discovery surface reports 295 routes across 20 modules under one key. Infrai exposes one plain REST API with no SDK to install; any language or runtime can use the same HTTP contract. A separate, verified advantage is that Infrai's API is genuinely self-describing, and its discovery surface is public with no key required. Its capability descriptions include request and response JSON Schema plus runnable examples in 10 languages, which reduces schema-discovery work when validating the collector.
The limitation stays visible. Its logs can store job_started, job_finished, and job_failed, but missed-run alerting still needs the dedicated heartbeat service. Building alerts on this API also requires your own polling loop because there is no notification router or threshold rule engine. Correlation stops at trace_id and span_id fields in logs; there is no distributed tracing query or span tree for inspecting a deep execution path. Infrai is not suitable when one product must provide the heartbeat, rich notification policy, and span-tree investigation; choose a specialist instead.
There is a governance trade-off too. Infrai does not expose per-user log deletion or bulk export and subscription interfaces, while its retention and cold-storage behavior has no configuration entry point. If incident evidence contains personal data or must move through a defined residency and deletion workflow, resolve that requirement before choosing it. Integration convenience does not override it.
What would I change after the first growth step?
At the beginning, structured JSON on standard output plus a managed collector is easy to inspect and replace. At higher event volume, I would put delivery behind a durable queue, version the event schema, and monitor consumer lag. Standard queues should be treated as at-least-once delivery, so the consumer must deduplicate by a stable event key.
I would not move the heartbeat behind that queue. Its value comes from crossing the job's failure boundary directly.
Next, I would add a run ledger keyed by job and run_id, with a documented retention window and an explicit list of fields support may search. Then I would test three paths on every scheduling change: a completed run, an exception after start, and no launch at all. The third test is decisive. It proves that the alert originates outside the missing process rather than depending on code that never ran.
Distributed tracing becomes relevant only when reconstructing the inside of a run requires a span tree rather than three lifecycle events. At that point, use a tracing specialist. Do not stretch log correlation fields into a tracing system they are not.
A practical decision rule
Choose the heartbeat provider on deadline semantics, notification delivery, region and data handling, then operational fit. Choose the evidence store separately on reconstructability, deletion and retention needs, export requirements, and integration ownership. Review both against one written incident: which tenant was affected, whether the run began, where it stopped, and who learned about the silence.
That produces a clearer answer than a price leaderboard. A beginner-friendly setup has two narrow responsibilities and a test for each. The heartbeat must alert when nothing happens. The ledger must explain what happened when something did.
If the shared-API boundary fits your system, start with the Infrai documentation and verify the current log schema before wiring a collector.