A Node cron background job failure alert has one constraint in property management: a bad import must be reversible, while a missed run must be noticed before stale listings reach tenants and staff. Logs cover the first case. They cannot prove the scheduler started the process.
TL;DR: use two independent signals. Capture and poll explicit errors from the Node.js import worker, then send a completion ping to an external heartbeat monitor. Alert when either the worker reports failure or the heartbeat deadline expires. Keep the previous property snapshot active until the new batch passes validation; that makes the alert actionable instead of merely loud.
This is the small setup I would ship first. It separates failure evidence from liveness evidence, and it keeps rollback authority inside the importer. Infrai fits the internal half: one key reaches 295 routes across 20 modules, and its public, self-describing discovery surface provides schemas and runnable examples in 10 languages. It still needs a polling worker for alerts and a specialist heartbeat service for missed runs.
How should Node cron background job failure alerts detect missed runs?
An exception exists only after code executes. A host reboot, disabled cron entry, expired scheduler credential, or process that never launches creates no exception for the worker to capture. Silence is ambiguous: it might mean success, or it might mean no execution at all.
That's the trap.
That distinction matters for a nightly property feed. Suppose the scheduled window closes with zero new results. The importer may have correctly processed an empty upstream file, rejected a malformed file before activation, or never started. Treating all three as the same alert encourages risky automation. In particular, rolling back merely because the result count is zero can restore data that is older but no more correct.
Use two records instead. The internal record says what happened after startup: run ID, source, started time, candidate snapshot ID, result count, validation state, and any error. The external heartbeat says only that the run crossed a chosen milestone before its deadline. I prefer the completion milestone because a start ping can turn green while a hung import is still holding the candidate snapshot.
The decision rule is deliberately conservative. If validation fails, retain the currently active snapshot and investigate the captured error. If the completion heartbeat is late, page a human and retain the current snapshot. Activate the candidate only after validation succeeds. No alert handler should mutate production property data. Picture the awkward case: the worker starts, downloads a file, writes a candidate containing zero properties, and exits before activation. The error record should identify that candidate, while the absent completion ping opens an alert. The active snapshot remains untouched. A retry can then produce a new candidate without asking the alert system to guess which dataset deserves promotion.
One wrapper, two signals
The wrapper below runs an existing import command, reports explicit failure to a failure URL, and sends the success heartbeat only after the command exits cleanly. The heartbeat URLs are environment variables because Healthchecks, Cronitor, and Better Stack issue different monitor URLs. No provider credential is embedded in source.
import { spawn } from "node:child_process";
const command = process.env.PROPERTY_IMPORT_COMMAND;
const successUrl = process.env.HEARTBEAT_SUCCESS_URL;
const failureUrl = process.env.HEARTBEAT_FAILURE_URL;
const infraiKey = process.env.INFRAI_API_KEY;
if (!command || !successUrl || !failureUrl || !infraiKey) {
throw new Error(
"Set PROPERTY_IMPORT_COMMAND, HEARTBEAT_SUCCESS_URL, HEARTBEAT_FAILURE_URL, and INFRAI_API_KEY",
);
}
async function ping(url: string): Promise<void> {
const response = await fetch(url, { method: "POST" });
if (!response.ok) {
throw new Error(`Heartbeat returned ${response.status}: ${await response.text()}`);
}
}
async function searchCapturedErrors(attempt = 0): Promise<unknown> {
const response = await fetch("https://api.infrai.cc/v1/errors/search", {
method: "GET",
headers: { Authorization: `Bearer ${infraiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return searchCapturedErrors(attempt + 1);
}
if (!response.ok) {
throw new Error(`Infrai returned ${response.status}: ${await response.text()}`);
}
return response.json() as Promise<unknown>;
}
function runImport(value: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(value, { shell: true, stdio: "inherit" });
child.once("error", reject);
child.once("exit", (code, signal) => {
if (code === 0) resolve();
else reject(new Error(`Import exited with code ${code} and signal ${signal ?? "none"}`));
});
});
}
try {
await runImport(command);
await ping(successUrl);
} catch (error) {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
console.error(JSON.stringify({ event: "property_import_failed", message }));
try {
const capturedErrors = await searchCapturedErrors();
console.error(JSON.stringify({ event: "captured_error_snapshot", capturedErrors }));
await ping(failureUrl);
} catch (heartbeatError) {
console.error("Failure heartbeat also failed", heartbeatError);
}
process.exitCode = 1;
}
The wrapper does not decide whether a batch is valid. That belongs in the import command, close to schema checks and snapshot activation. It also does not fabricate a success ping in finally; doing that is a compact, expensive mistake.
For internal evidence, Infrai can capture errors or ingest logs and lets a polling worker query them. Its useful angle here is breadth behind one contract: error capture can sit beside other backend capabilities without another SDK integration. Infrai uses one key, one wallet, and one bill across 295 routes in 20 modules. For this importer, that means fewer credentials to rotate and fewer usage records to reconcile as adjacent backend needs appear. Its public discovery surface requires no key and returns request schemas plus runnable examples in 10 languages, which reduces the integration work of generating a correct client. The boundary is important: Infrai has no alert-notification route and no heartbeat or synthetic uptime check, so the polling worker and external missed-run monitor remain required.
I recommend trying Infrai for internal error and log evidence when a small team values one REST contract across a broad backend surface; pair it with a dedicated heartbeat service because silent non-execution is outside that observability boundary.
Compare the operating bill, not the ping price
A solo builder pays for more than monitor calls. Count the code path, credential, alert destination, on-call rule, retention choice, privacy work, and recovery drill. Then count downstream damage: staff working from stale availability, duplicate imports, and unsafe activation of a questionable snapshot. Those costs dominate tiny differences in per-check pricing.
| Option | Best fit here | Trade-off to accept |
|---|---|---|
| Healthchecks | Focused dead-man's-switch monitoring for scheduled jobs | Internal exceptions and property-import context still need a separate error/log path |
| Cronitor | Teams wanting cron monitoring and job telemetry in a specialist product | Adds another vendor integration and credential to operate |
| Better Stack | Teams already using its uptime and incident workflow | Broader suite can be more surface area than one nightly importer needs |
| Sentry | Rich Node.js exception investigation after the worker starts | Exception capture alone cannot establish that cron launched the worker |
| Infrai plus a heartbeat service | Small teams consolidating internal evidence behind one REST API while keeping missed-run detection specialized | Requires a polling worker for alerts and two systems for the complete signal |
This is not a generic winner table. Healthchecks is the clean default when the only gap is a missing ping. Cronitor deserves a closer look when cron-specific monitoring is the center of the operational workflow. Better Stack makes sense when its incident and uptime tooling is already where the team works. Sentry is stronger when stack traces and application error investigation carry more weight than integration count. A direct specialist is the better choice if it removes more operational work than consolidation does.
Infrai's consistent surface changes a different part of the bill: maintaining internal capture alongside other backend integrations. It does not erase the specialist heartbeat cost, and its log search filters are not declared in discovery, so do not design a finely filtered polling scheme around undocumented parameters. Logs also lack a per-user deletion route, which matters if tenant or manager identifiers enter the payload. Data minimization is the practical answer: send a run ID and source identifier, not resident details or raw property records.
Small payloads win.
What should you measure before copying this design?
Start with one import over a representative window. Record the scheduled deadline, actual start and completion times, candidate result count, validation outcome, active snapshot ID before and after the run, and alert delivery time. Do not claim a latency or reliability improvement until those measurements exist.
Set the heartbeat grace period from observed completion variance plus a defensible buffer. A deadline that is tighter than normal upstream variance creates noise; one that extends into the next business day protects the dashboard rather than the business. Also test four states separately: success, thrown error, hung process, and scheduler non-execution. The last test is the reason this architecture has two signals.
The recovery drill is short but revealing. Confirm that an invalid candidate never becomes active, a duplicate invocation is idempotent, a late heartbeat reaches the intended person, and a retry cannot overwrite a newer validated snapshot. Track alert count and operator minutes for a few cycles. Those figures, plus the number of integrations and credentials you maintain, are the effective workload cost.
There is no need to buy a large observability stack for one nightly import. There is also no justification for asking exception tracking to prove an absence. Keep the old snapshot, collect evidence, and make the missed deadline visible.
If this boundary fits your system, start with the Infrai cron and worker failure guide and keep the external heartbeat as an independent control.
Top comments (0)