Use a deadline-based heartbeat to detect that a checkout task never ran, then use application metrics to explain the work and assign its cost. TL;DR: for a Node.js SaaS operating in US and EU regions, this is a layered decision, not a contest between two interchangeable tools. A heartbeat owns absence detection. Metrics own volume, duration, errors, and cost attribution after execution begins.
The tempting first design is a single last_success gauge in the application metrics stream. It looks cheap because the metrics pipeline already exists. It also asks the wrong component to infer silence: a stopped scheduler, a broken process, and a failed metrics exporter can all produce the same empty chart. For a checkout recovery job, that ambiguity delays the only answer that matters at first: did the expected run arrive before its deadline?
My decision constraint is narrow: every signal must say which scheduled workload created an observability cost, without attaching customer or payment data. The focused example below is a recovery task scheduled every 15 minutes, split by us and eu, with a 10-minute grace period. Those numbers are configuration choices for the example, not universal thresholds.
1. Should I Choose Healthchecks or App Metrics for Missed Cron Jobs?
A heartbeat is an external expectation: the monitor expects a small event for a named task within a defined window. When no event arrives, the deadline itself creates the alert. That makes it the cleanest signal for a missed scheduled task, because detection does not require the task to start, register a metric, or reach its success path.
Silence is the signal.
Send the heartbeat only after the task has completed the unit of work represented by the schedule. A ping at process start proves that a process woke up; it does not prove the checkout recovery pass finished. If execution can legitimately run longer than the interval, model start and completion separately or prevent overlap. Otherwise, a late completion can look like a missing run while the previous run is still working.
Keep the payload dull. OWASP's logging guidance warns that logs may contain sensitive data and recommends excluding or masking data that should not be recorded. The same discipline belongs on heartbeat URLs and labels: use a stable task key and region, never a cart token, email address, payment identifier, session ID, or raw exception.
One short event is enough.
2. Give metrics the questions they can actually answer
Once the job starts, application metrics become useful. Counters can represent attempted, recovered, deferred, and failed checkout records. A duration distribution can show whether a run is approaching its next scheduled window. Those signals explain impact; they do not independently establish that an absent process missed its appointment.
This distinction avoids a common trap. A zero-value metric can mean zero eligible checkouts, but a missing time series can mean the task never ran, the exporter failed, or the label set changed. Treating all three as “zero” hides the scheduler failure. Treating all three as “page now” creates noise. The heartbeat resolves execution presence; metrics preserve the operational context.
For this workload, the useful metric dimensions are deliberately bounded:
| Signal | Stable dimensions | Question answered |
|---|---|---|
| Completion heartbeat | task, region, environment | Did the scheduled pass finish on time? |
| Attempt counter | task, region, outcome | How much checkout work was evaluated? |
| Duration distribution | task, region | Is runtime consuming the scheduling window? |
| Failure counter | task, region, failure class | Which broad failure mode is growing? |
Do not add checkout_id as a metric label. It makes cost track the number of individual checkouts rather than the small set of operational decisions, and it places a business identifier in a system that does not need it. Put record-level detail in access-controlled application logs only when it is necessary, with masking and retention rules aligned to the data's sensitivity.
3. Attribute spend before adding more telemetry
Cost attribution works when the heartbeat and metric dimensions share a small vocabulary. I would use task=checkout_recovery, region=us|eu, and environment=production|staging for the external completion signal. Application metrics can add a bounded outcome or failure_class, but the base dimensions stay identical. This lets an operator allocate event volume, stored series, alert evaluations, and investigation effort to the scheduled workload that caused them.
The trade-off is blunt: richer dimensions improve slicing, while every additional value expands storage and query work. Customer-level attribution does not belong in metrics. If the business needs per-merchant cost accounting, calculate that in the checkout data plane and emit a controlled aggregate; do not turn the monitoring plane into a second transaction database.
Here is a small TypeScript boundary that keeps detection separate from explanation. The endpoint is intentionally generic, and the example sends no checkout data.
type Region = "us" | "eu"; type Outcome = "recovered" | "deferred" | "failed"; interface Metrics { increment(name: string, labels: Record<string, string>, value?: number): void; observe(name: string, value: number, labels: Record<string, string>): void; } async function reportCompletion(task: string, region: Region): Promise<void> { const response = await fetch(process.env.HEARTBEAT_ENDPOINT!, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ task, region, status: "complete" }), }); if (!response.ok) throw new Error(`heartbeat rejected: ${response.status}`); } async function runCheckoutRecovery(region: Region, metrics: Metrics): Promise<void> { const task = "checkout_recovery"; const startedAt = performance.now(); const outcomes: Record<Outcome, number> = { recovered: 0, deferred: 0, failed: 0, }; // Business processing updates only these bounded outcome buckets. for (const outcome of Object.keys(outcomes) as Outcome[]) { metrics.increment("scheduled_task_records", { task, region, outcome }, outcomes[outcome]); } metrics.observe("scheduled_task_duration_ms", performance.now() - startedAt, { task, region, }); await reportCompletion(task, region); } In production, the heartbeat request needs a timeout and an explicit retry policy. Keep retries inside the grace window, and make a monitoring-delivery failure visible through a separate bounded error counter or log event. Do not report completion in a finally block; that converts failed business work into a healthy schedule.
4. Test the silent paths, not just successful execution
The experiment should compare failure signatures, not dashboard appearance. Stop the scheduler before invocation and confirm the heartbeat deadline detects it. Start the task but fail before business processing; the completion heartbeat must remain absent. Complete a run with zero eligible checkout records; the heartbeat should arrive while attempt counters remain at zero. Finally, interrupt metric export after successful work and verify that missed-run detection still has an independent path.
These four cases separate scheduler availability, business outcome, and telemetry delivery. They also expose alert coupling before deployment. A single synthetic test per region is more informative than adding more labels to production traffic, because it checks the deadline from outside the worker. For US and EU execution, configure and test each regional schedule independently. Shared naming enables comparison, while the region dimension makes ownership and event volume visible. Avoid copying raw application context across regions merely to enrich an alert; the alert needs enough information to route the failure, not reconstruct a checkout. Run the same matrix after changing the schedule, grace period, worker concurrency, or telemetry transport, since each change can alter which deadline fires first without changing any checkout code.
That boundary matters.
5. Measure this before copying the design
Choose the heartbeat deadline from the real schedule, worst acceptable completion delay, and overlap policy. Then measure late-but-valid runs, duplicate invocations, heartbeat delivery failures, metric series count, event volume by task and region, and pages that required no action. A design that detects every delay but pages on normal overlap is not finished.
Also track the investigation path. If responders consistently need a task name, region, run timestamp, outcome counts, and duration, those fields belong in the correlation model. If they keep asking for individual checkout records, link to the restricted application logs by an opaque run identifier instead of copying sensitive fields into metrics or heartbeat events. OWASP's guidance on sanitization, access control, and log-data protection is a practical baseline here.
Stop there.
The resulting choice stays small: one deadline signal per scheduled regional task, a handful of bounded application metrics, and protected logs for record-level diagnosis. No single stream has to impersonate the others. More important for a solo operator, observability cost remains attributable to a workload and region before new dashboards or alerts multiply it.
References
- OWASP Logging Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
- Logback Manual, Appenders: https://logback.qos.ch/manual/appenders.html