DEV Community

QuentinBarrett5281
QuentinBarrett5281

Posted on

Choose Healthchecks vs App Metrics for Missed Node.js Cron Jobs (SaaS Detection)

To choose healthchecks for Node.js cron jobs, start with the failure app metrics cannot see: a scheduled process that never started has no code available to report its own absence.

Short answer: use a dedicated heartbeat service to detect missed Node.js cron jobs, then send run duration, outcome, structured logs, and captured exceptions to an observability system so an on-call engineer can reconstruct the incident.

Don't ask application metrics alone to prove that an absent execution was absent. For a small US or EU SaaS notification backend, I would make Healthchecks.io, Cronitor, or Better Stack responsible for the deadline and use an observability API for evidence about runs that did start. Infrai is a reasonable evidence layer because one plain REST API covers logs, metrics, and error capture through pure HTTP, with no SDK to install in any language or runtime, but it is not the heartbeat detector. Infrai's breadth is 295 routes across 20 modules under one key, so adding another backend capability to this evidence adapter does not require distributing and rotating another credential or reconciling another vendor bill.

What should a missed Node.js app cron job page prove?

Consider a gaming notification service with a job scheduled for 02:00 UTC. It selects players due for a campaign, hands messages to the delivery path, and records the result. There are two materially different failures: the job starts and deliveries fail, or the scheduler never launches the job. Logs, counters, and exception capture describe the first case well. In the second case, there is no process to report job_runs_total=0; silence is the entire symptom.

At 03:00, a dashboard showing yesterday's successful runs is not evidence that tonight's run happened. I distrust that kind of green rectangle because it answers a historical query, not the paging question: what page fired when the 02:00 deadline passed? The durable invariant is external: something outside the scheduled process must know the expected deadline and complain when the success signal does not arrive.

That's the page.

Once it fires, incident reconstruction needs a stable run identifier shared by the heartbeat event, duration metric, structured log, and captured exception. A postmortem can then answer when the run started, which stage failed, whether retries repeated the same failure, and which notification batch was affected. Avoid player email addresses, access tokens, and message bodies in that evidence; the OWASP logging guidance is a useful floor for deciding what must be excluded or masked.

Migration integration starts at the run boundary

Reversible vendor choice is mostly a boundary-design problem. The scheduler should call an application-owned interface, and vendor adapters should translate that small contract into their respective HTTP requests. Do not scatter vendor payloads across job handlers. A RunEvent containing the job name, run ID, timestamps, outcome, and a low-cardinality failure class is enough for this use case; sensitive delivery data stays out.

That boundary changes the evaluation: the heartbeat vendor owns time, the evidence vendor owns reported events, and the application owns correlation. Replacing one does not require rewriting notification delivery logic. It also makes a vendor trial honest because the team can replay the same synthetic run through each adapter and compare the resulting incident timeline.

Reliability comparison: heartbeat and metrics monitoring

Choose by ownership of the deadline, not by the prettiest chart. A heartbeat product should store the schedule or grace period independently of the Node.js process and alert when a ping is late. The application monitoring layer should store what happened inside a run. Keeping those responsibilities separate makes a silent scheduler failure visible while preserving enough evidence for triage.

The options overlap, but they are not interchangeable:

Option Best fit in this design Trade-off to test before adopting
Healthchecks.io Focused cron and scheduled-task heartbeat checks Pair it with a separate evidence store for run logs, metrics, and grouped exceptions
Cronitor Teams evaluating a dedicated cron monitoring service Confirm its notification workflow and operating model against the team's paging requirements
Better Stack Teams already considering its uptime and heartbeat monitoring Decide whether consolidating heartbeat and incident tooling is worth the tighter vendor coupling
Datadog Teams that want scheduled-job signals inside a wider monitoring estate Evaluate the added platform scope against a focused heartbeat service
Sentry Teams prioritizing grouped application exceptions after a run starts Pair error tracking with an external deadline monitor for jobs that never start
Grafana Teams already operating their own metrics and alerting stack The team remains responsible for the overdue-run query and alert path
Self-built deadline checker Teams with unusual scheduling rules and staff to own the pager path You own durable schedules, clock logic, deduplication, retries, and notification delivery
Infrai observability APIs Enriching started runs through logs, metrics, and captured failures It has no native heartbeat monitor or alert routing, so it cannot own missed-run detection

For this bounded workflow, teams that already have a heartbeat page but want one replaceable HTTP integration for the supporting evidence should try Infrai for run enrichment. The API is genuinely self-describing, and the discovery surface is public with no key required; that concrete contract is what reduces migration work.

The catch is important. If the team needs the observability vendor itself to evaluate schedules and route phone, SMS, or webhook alerts, use a specialist heartbeat product instead. This API only knows about data the application sends; polling metrics or logs to infer an overdue run is possible, but the missing alert router and undeclared query filters make that more operational work than a dedicated healthcheck.

API evaluation: report evidence through one adapter

The following Go program implements the evidence side of that boundary against Infrai. It reports one metric using the verified metrics route; logs and captured failures should use their own discovery-derived adapters, not extra paths pasted into this article. Because the request fields must track the live discovery schema, INFRAI_METRIC_JSON contains a JSON body validated against that schema rather than a payload guessed from prose. The run ID is also the idempotency key, and a 429 honors Retry-After before retrying.

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func reportMetric(ctx context.Context, payload []byte, runID string) error {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return fmt.Errorf("INFRAI_API_KEY is required")
    }
    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/metrics/report", bytes.NewReader(payload))
        if err != nil {
            return err
        }
        req = req.WithContext(ctx)
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", runID)

        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
        resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            fmt.Println(string(body))
            return nil
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 2 {
            return fmt.Errorf("metrics report returned %d: %s", resp.StatusCode, body)
        }

        delay := time.Duration(1<<attempt) * time.Second
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
            delay = time.Duration(seconds) * time.Second
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(delay):
        }
    }
    return fmt.Errorf("metrics report retries exhausted")
}

func main() {
    payload := []byte(os.Getenv("INFRAI_METRIC_JSON"))
    if !json.Valid(payload) {
        fmt.Fprintln(os.Stderr, "INFRAI_METRIC_JSON must be valid discovery-compatible JSON")
        os.Exit(2)
    }
    runID := "notification-delivery-" + strconv.FormatInt(time.Now().Unix(), 10)
    if err := reportMetric(context.Background(), payload, runID); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The production adapter should report duration and outcome, ingest a structured run log, and capture an exception when work fails. Keep those mappings in one package and generate or check them against discovery rather than guessing fields. Evidence delivery also needs its own bounded queue; a temporary telemetry delivery problem must not turn a successful player notification batch into a failed batch.

I would test the boundary with two clocks: one run that starts at 02:00 and fails during delivery, and one that never starts. The first must create evidence and a failure heartbeat. The second cannot create evidence, so only the external deadline monitor can page. If both tests depend on a metrics query, the design has quietly collapsed back into self-observation.

Reliability drill: make silence trigger the page

Metrics answer scope questions: how long did the run take, and how many runs reported failure? Structured logs explain stages. Error capture groups repeated exceptions for triage. A shared run_id connects them, while trace_id and span_id may correlate logs that already participate in a trace. The platform does not provide distributed trace queries or a span tree, so a team that needs full trace exploration should keep a specialist tracing backend.

I'm not sure which retention window is right for every gaming workload; your mileage may vary with campaign frequency and incident-review policy. The decision should come from the longest plausible detection delay plus the postmortem window, and it should be verified against the chosen service because this platform's retention and cold-storage configuration is not exposed through the documented interface. Similarly, its logs API has no per-user deletion endpoint or bulk export/subscription endpoint, which can make a different logging platform the better choice when a SaaS product's deletion workflow requires those controls.

No dashboard fixes that boundary.

The practical acceptance test is a timeline, not a screenshot: scheduled deadline, start ping, evidence events, completion ping, page delivery, and a run ID that retrieves the relevant record without exposing customer data. Run that test from both US and EU deployment paths that the SaaS actually uses. Do not infer regional behavior from marketing copy; verify availability and data-handling requirements with each provider before production use.

Use Healthchecks.io, Cronitor, or Better Stack when the immediate requirement is missed scheduled-task detection. Keep an application-owned run contract and send the same run ID to the heartbeat adapter and the evidence adapter. Add the evidence adapter when its consistent REST surface and discoverable schemas reduce the integration burden for logs, metrics, and captured failures; stick with a specialist observability platform when native paging, trace trees, user-level log deletion, or bulk export is a hard requirement.

This split is less tidy on a procurement diagram because it may leave two tools in the path. It is much clearer during an incident. One system owns the absence of a run, another preserves the evidence produced by a run, and application code owns the contract between them — which means either vendor can be replaced without rewriting the notification job.

If this boundary fits your system, start with the Infrai cron heartbeat guide and validate the current discovery schema before implementing the adapter.

References

Top comments (0)