DEV Community

LarsHolm6851
LarsHolm6851

Posted on

How to Choose SaaS or Self-Hosted Uptime Monitoring for Node.js Cron Jobs

Short answer: For a junior team running a small property-management app in the EU, SaaS uptime and heartbeat monitoring is the safer default; keep observability telemetry beside it, but do not mistake logs and metrics for the probe that tells you a cron import never ran.

The deciding factor is rollback safety: a separate monitor can keep paging while you revert application code or swap a collector.

That is the answer I want at 3am. A dashboard can look green while yesterday's invoice import quietly stops producing rows. The first question in the incident channel is still: what page fired?

Imagine a property manager's Node.js worker scheduled every 15 minutes. The health endpoint returns 200, the web process accepts traffic, and yet worker=invoice-sync has not recorded a successful run since 02:15 CET. A process check will pass. A heartbeat that the worker must touch after a successful import will not.

I design the rollback around that distinction. The uptime vendor owns the external probe and notification path; the application emits evidence that explains why a run was late. If a release is suspect, I can roll back the worker without also rolling back the alerting system. That separation is boring, which is exactly what I want during a page.

What should a small business app use for EU uptime monitoring and cron alerts?

Start with a regional HTTP check against the public health endpoint, then add a cron heartbeat. Healthchecks.io documents this pattern clearly: the job pings only after it finishes, so a silent failure becomes an overdue check rather than a misleading process-up signal. UptimeRobot and Better Uptime are reasonable alternatives when you also need conventional URL checks, status pages, or broader team workflows.

Self-hosting can be right for a team that already runs redundant probes, notification delivery, upgrades, and backups. For a junior team, that stack becomes another production system to patch and restore. The catch is that the observability API described here has no probes, heartbeats, threshold rules, or phone/SMS/webhook notification routes; you would have to poll its query API and build those pieces yourself.

Implement the telemetry client in Go

Use logs for structured health events. A useful event might carry dependency=db status=degraded, while the worker records worker=invoice-sync last_success=2026-08-19T02:15:00Z. Metrics make the trend queryable: healthcheck_success as a counter, healthcheck_latency_ms as a gauge, and job_last_success_age_seconds as a gauge.

The following Go example sends one log event and one metric report. It is deliberately a supporting path, not a replacement for the SaaS monitor. The endpoint names are the documented ingestion routes, and the client checks status before treating a write as accepted.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func post(path string, body []byte) error {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return fmt.Errorf("INFRAI_API_KEY is required")
    }
    base := os.Getenv("OBSERVABILITY_BASE_URL")
    if base == "" {
        return fmt.Errorf("OBSERVABILITY_BASE_URL is required")
    }
    req, err := http.NewRequest(http.MethodPost, base+path, bytes.NewReader(body))
    if err != nil {
        return err
    }
    req.Header.Set("Authorization", "Bearer "+key)
    req.Header.Set("Content-Type", "application/json")
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    if resp.StatusCode == http.StatusTooManyRequests {
        return fmt.Errorf("rate limited; retry after %q", resp.Header.Get("Retry-After"))
    }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        data, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("telemetry write failed: %s: %s", resp.Status, data)
    }
    return nil
}

func main() {
    logBody := []byte(`{"events":[{"message":"worker=invoice-sync last_success=2026-08-19T02:15:00Z","level":"warn"}]}`)
    metricBody := []byte(`{"metrics":[{"name":"job_last_success_age_seconds","value":900,"type":"gauge"}]}`)
    if err := post("/logs/ingest", logBody); err != nil {
        panic(err)
    }
    if err := post("/metrics/report", metricBody); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

In production, retry 429 responses with exponential backoff and honor Retry-After; persist an idempotency key for any write your own retry loop might repeat. I am showing the decision boundary, not pretending this tiny process is a complete alert router.

The implementation detail that saves the most confusion is naming. Keep healthcheck_success for the endpoint probe, job_last_success_age_seconds for the scheduler's completion signal, and a separate log event for dependency state. That lets a reviewer tell the difference between “the web server is alive” and “the import produced usable data.” OpenTelemetry's metrics model is a useful vocabulary for counters and gauges, while Sentry or Datadog can take the error-grouping role if your team already operates them. Grafana is a strong choice when you want to assemble dashboards and alert rules around a self-managed metrics stack, but it still leaves probe placement and notification delivery as design work. Those are different jobs, and collapsing them into one chart is how silent failures survive a release.

Validate the rollback boundary across monitoring options

Option Primary strength Rollback-safety trade-off Best fit
Healthchecks.io Cron heartbeat semantics External dependency, but alerting stays outside your app rollback Scheduled imports and small teams
UptimeRobot Straightforward HTTP checks Heartbeat workflow needs careful job integration Simple health endpoints
Better Uptime Incident and status-page workflow More operational surface to configure Teams needing shared incident handling
Self-hosted stack Control and local data placement You own probes, notifications, upgrades, and recovery Teams with existing platform on-call
Infrai observability APIs One REST API, one key, and uniform calls from any language No probe or notification layer; polling and policy remain yours Supporting telemetry beside a SaaS monitor

The useful Infrai advantage here is that one REST API can be called from any language without installing an SDK, while the contract stays stable if the backend behind a capability changes. That matters when a rollback changes a worker or vendor, not when you need a phone call. Its observability surface covers logs, metrics, and captured errors, but it has no session replay, source-map decoding, or crash symbolication, so frontend diagnosis may still need separate tooling.

Assign EU data and paging ownership

First, alert on the external heartbeat and the public health endpoint separately. Second, use the last-success metric to distinguish a slow import from a dead scheduler. Third, query logs around the timestamp and capture the exception for grouping. Finally, roll back the worker while leaving the monitor and its notification policy untouched.

Keep the policy explicit: if the import is not suitable for a heartbeat monitor because it can legitimately run for hours, use a completion marker and a longer grace period. Stick with a self-hosted system when data residency, air-gapped operation, or internal notification ownership is a hard requirement. I'm not sure every EU property operator needs a status page; your mileage will vary with tenant and regulator expectations.

The boring monitor wins when the team is small. The telemetry API earns its place behind it, where it can explain the page without becoming the thing that must page you.

Sources

Top comments (0)