Short answer: use an external heartbeat when the primary requirement is the simplest alert for a missed cron run; use a custom metrics API when you already operate an absence rule and need richer cost attribution. For a marketplace data pipeline, the dependable design is usually both: a heartbeat answers “did the expected run finish?”, while structured metrics and logs answer “what did it cost, and where did it spend time?”
| Option | Best question answered | Choose it when | Main trade-off |
|---|---|---|---|
| External heartbeat | Did the expected run check in? | A small team needs missed-run email or webhook alerting | It gives little diagnostic detail by itself |
| Custom metrics API | What happened during each run? | An existing alert engine already evaluates missing data | You own the schedule model, rule, retries, and notification path |
| Both signals | Did it finish, and why did it cost that much? | A nightly pipeline affects marketplace operations | Two integrations need consistent run IDs and ownership |
Absence is the problem. A job that never starts cannot emit success = 0, write a failure log, or increment an error counter. A metrics store can faithfully record every point it receives and still show nothing unusual. Something outside the job has to know that silence was unexpected.
Silence needs an owner.
What should Node.js cron monitoring measure before it alerts?
Start with a schedule contract, not a dashboard. Write down the expected time zone, allowed start window, maximum runtime, and the event that counts as completion. “Nightly” is not precise enough for an alert rule when the marketplace has EU and US tenants, daylight-saving changes, or a pipeline whose input volume varies.
For each invocation, carry a stable run_id. Record the schedule date, region, started-at time, completed-at time, outcome, item count, and a cost attribution key such as tenant, marketplace, or pipeline stage. Keep sensitive records out of labels. High-cardinality identifiers make queries harder and can expose more data than an operator needs.
The signal vocabulary stays small:
- A heartbeat is liveness. It proves that a checkpoint happened before its deadline.
- A metric is a trend or measurement, such as duration, rows processed, or estimated compute units.
- A log is evidence, including the stage and correlation identifiers needed to investigate.
- An error event is a failure that the system actually observed.
Those signals are related, but they are not interchangeable. A completed run with an unexpectedly high cost needs metrics and logs. A run that never launched needs an external clock.
How do heartbeat and custom metrics paths catch a missed cron run?
Picture the architecture in words: the scheduler starts the Node.js process; the process creates a run_id; each stage emits structured evidence; a successful completion sends the heartbeat; an external monitor watches the deadline; a metrics evaluator and notification channel handle richer rules. If the process never starts, the external clock still advances. If it starts and stalls, a start marker and a missing completion marker distinguish that state. The distinction matters during triage: “no invocation” points toward the scheduler or deployment, “started but no completion” points toward a dependency or runtime limit, and “completed at high cost” points toward input growth or a changed stage. One timestamp cannot carry all three meanings, so avoid compressing them into a single green-or-red metric.
Here is a provider-neutral wrapper. It treats the heartbeat URL as configuration, sends it only after the business work succeeds, and keeps the primary result separate from telemetry delivery. The exact URL and authentication contract belong to the service selected by the team; the application should not guess them.
type RunContext = {
runId: string;
region: "eu" | "us";
scheduleDate: string;
};
type RunSummary = RunContext & {
durationMs: number;
rowsProcessed: number;
outcome: "success" | "failure";
};
const heartbeatUrl = process.env.HEARTBEAT_URL;
if (!heartbeatUrl) {
throw new Error("HEARTBEAT_URL is required");
}
async function sendHeartbeat(): Promise<void> {
const response = await fetch(heartbeatUrl, { method: "POST" });
if (!response.ok) {
throw new Error(`Heartbeat rejected: ${response.status}`);
}
}
async function runNightlyPipeline(context: RunContext): Promise<RunSummary> {
const startedAt = Date.now();
let rowsProcessed = 0;
try {
rowsProcessed = await processMarketplaceRecords(context);
const summary: RunSummary = {
...context,
durationMs: Date.now() - startedAt,
rowsProcessed,
outcome: "success",
};
await writeStructuredTelemetry(summary);
await sendHeartbeat();
return summary;
} catch (error) {
const summary: RunSummary = {
...context,
durationMs: Date.now() - startedAt,
rowsProcessed,
outcome: "failure",
};
await writeStructuredTelemetry(summary, error);
throw error;
}
}
async function processMarketplaceRecords(context: RunContext): Promise<number> {
// Replace this with the pipeline's actual work.
return context.region === "eu" ? 0 : 0;
}
async function writeStructuredTelemetry(
summary: RunSummary,
error?: unknown,
): Promise<void> {
// Send summary and error details to the team's chosen telemetry store.
void summary;
void error;
}
const context: RunContext = {
runId: crypto.randomUUID(),
region: "eu",
scheduleDate: new Date().toISOString().slice(0, 10),
};
await runNightlyPipeline(context);
The snippet deliberately makes one policy visible: a heartbeat after success is a dead-man check for completion. A failed run can still produce a failure metric and log, while the missing success heartbeat keeps the alert condition simple. To detect a process that starts and then hangs, add a documented start checkpoint or a bounded execution timeout, but keep the state names explicit: started, succeeded, and failed.
Do not let a telemetry retry hold the business job forever. Bound retries, honor the selected transport’s rate-limit guidance, and preserve the original run outcome if the secondary write cannot be delivered. Alert routing is part of the design, too: an alert that nobody owns is only a log message with better typography.
How does cost attribution change the observability design?
Cost attribution needs dimensions that a heartbeat intentionally does not carry. A useful event might associate run_id with region, pipeline stage, tenant class, rows processed, duration, and an internal cost unit. That lets an operator compare a slow EU enrichment stage with a normal US aggregation stage without pretending that duration alone is a bill.
The accounting model should be defined before the metric names. Decide which values are measured directly, which are estimates, and which are allocated from a shared bill. Then keep the calculation reproducible. A simple report can join run telemetry to a rate table owned by finance or platform engineering; it should not hide changing rates inside a dashboard formula that nobody tests.
This is where custom metrics earn their place. They can support questions such as “which stage consumed the most work last week?” and “did rows processed grow faster than duration?” They still need an evaluator for missed data. A missing metric is not automatically a missed run: ingestion can be delayed, a label can be wrong, or the schedule can have been intentionally paused.
Test the rule with three cases: skip the invocation entirely, start and hang, and complete with an unusually high cost unit. The first should trigger the external deadline check. The second should trigger the runtime policy. The third should page only if the cost threshold is operationally meaningful. This is a much better test matrix than waiting for production to reveal which absence the team meant.
What are the limits of the simplest missed-cron alert?
An external heartbeat is not suitable when a team already has a dependable alert-rule engine, schedule-aware evaluation, and notification ownership. In that case, a custom metric plus an explicit missing-data rule may reduce integrations. Stick with the existing control plane when it already handles the hard parts and the added heartbeat would only duplicate them.
The heartbeat path is also too thin for distributed trace navigation, source-map decoding, crash symbolization, or detailed per-user deletion and export workflows. Select storage and investigation tools according to those requirements. Your mileage may vary across EU and US deployments: residency, processing locations, and transfer terms differ by service and contract, so check current regional and contractual documentation before sending tenant-level metadata across regions.
The final decision rule is modest. Use an external deadline for absence, structured telemetry for explanation, and a tested cost model for attribution. Keep the signals independent enough that a dead process cannot silence its own alarm.
Top comments (0)