DEV Community

RivenPulse5812
RivenPulse5812

Posted on

Node.js Startup Monitoring: 3 Uptime Checks Plus a Hosted Status Page

An e-commerce startup app can pass US and EU health checks all night while importing zero products. That constraint changes the setup: use external uptime monitoring plus a hosted status page for customer-facing availability, and add a dead-man heartbeat for the scheduled job itself.

TL;DR: treat this as three signals, not one dashboard. Probe the US and EU storefront paths from outside your network. Require each scheduled Node.js import to check in only after it produces a valid result. Publish confirmed customer impact through a hosted incident page. Keep application logs and metrics for investigation, but do not mistake telemetry storage for alert delivery.

That split is slightly less tidy than buying an “observability” label. It is much more honest.

Should startup uptime monitoring include a status page plus health checks?

An HTTP probe answers a narrow question: can this endpoint respond from a particular region? It cannot prove that the 02:00 supplier feed ran, parsed 18,420 rows, and committed fresh inventory. A /health handler can be green while yesterday's catalog remains in Postgres.

The useful unit is an outcome. For every scheduled import, record the expected run identifier, start time, finish time, source, rows accepted, rows rejected, and a tenant or store identifier. Send the heartbeat after the commit, not when the process starts. If the process never runs, crashes before commit, or completes without a result, the missing check-in becomes the alert.

This is where I reject the all-in-one instinct. There is no built-in status page, synthetic probe, heartbeat monitor, or notification router in a telemetry API alone. Logs and metrics help explain an incident after another service detects it; they cannot reliably infer silent non-execution. Repeated dependency exceptions can also be grouped for diagnosis, but error grouping still does not prove that a scheduler fired.

No heartbeat, no proof.

The customer view is different again. A hosted status page should describe the affected capability, such as “EU catalog freshness,” rather than expose an internal worker name. Someone must decide when degraded data freshness becomes a public incident. Automating that decision from one failed check creates noisy incident history.

The constraint that changes the shortlist

Cost attribution matters more than a low headline price. A startup needs to answer which store, region, import, and dependency produced the monitoring load. If a vendor gives one undifferentiated monthly total, finance gets a bill and engineering gets a guessing exercise.

I would shortlist these products by responsibility, then verify current retention, check frequency, region coverage, status-page limits, and notification channels in their live documentation before signing a contract:

Product Best fit here Boundary to account for Cost-attribution check
Better Stack Uptime checks, on-call flow, and a hosted status page in one operational product The import still needs an explicit heartbeat or outcome signal Confirm whether monitors and incidents can be grouped by store and environment
UptimeRobot A small, conventional uptime and status-page setup Application telemetry and detailed import diagnosis remain elsewhere Map monitors to stable store/region tags before the list grows
Checkly Programmable API and browser checks close to a developer workflow Browser checks are the wrong primitive for proving a cron job completed Review check-level usage and separate US/EU test ownership
Healthchecks.io Dead-man monitoring for cron jobs and workers It is not the full customer incident-communication layer Use one check identity per import and environment
Atlassian Statuspage Mature public incident communication when that is the main requirement Detection and job heartbeats must come from other tools Decide which business component owns each incident before launch
Datadog Broad infrastructure and application monitoring when one team needs cross-system correlation More surface area and configuration than a tiny startup needs for one import Require store and region tags on every monitor and emitted event
Grafana Dashboards and alerting where the team already operates a metrics stack A dashboard is not a hosted customer incident workflow by itself Preserve tenant labels through collection and aggregation
Sentry Grouping repeated application errors after a dependency fails Error capture does not detect a job that never started Tag import errors by job and environment, without customer data

No row wins every column. Better Stack is attractive when reducing alert and status-page glue matters. UptimeRobot suits a simpler probe-first setup. Checkly earns its keep when checks belong in code review and browser behavior matters. Healthchecks.io is the sharpest single-purpose answer to “did the job run?” Atlassian Statuspage makes sense when incident communication is already a distinct operational function.

For backend telemetry, Infrai can consolidate logs, metrics, and other backend capabilities behind one key and one bill, which makes service-level attribution easier to reconcile than a pile of unrelated credentials and invoices. It exposes a plain REST interface, so this Node.js worker does not need another vendor SDK. Its public, keyless discovery surface describes 295 capabilities across 20 modules, and documented capabilities have runnable examples in 10 languages. That self-description cuts setup friction when the worker and the monitoring utility ship on different runtimes. It is still not the uptime or incident layer in this design. Filtering for log search and metric queries may require validation because those filter parameters are not fully declared in discovery.

Infrai's other relevant advantage is one plain REST API with no SDK to install: the same HTTP client works from any language or runtime. The API is genuinely self-describing, its discovery surface is public with no key required, and 295 routes span 20 modules. For this workflow, that means a small monitoring utility can inspect a capability before calling it instead of carrying another generated client and its configuration.

The smallest Node.js heartbeat that earns its check-in

The following wrapper is intentionally boring. It runs a real import command, requires a machine-readable result, emits one structured record, and pings a vendor-issued heartbeat URL only after useful work completes. The URL comes from an environment variable, so the same code works with a dedicated heartbeat service or another monitor that accepts check-ins.

import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";

type ImportResult = {
  storeId: string;
  region: "us" | "eu";
  accepted: number;
  rejected: number;
};

function runImport(): Promise<ImportResult> {
  return new Promise((resolve, reject) => {
    const child = spawn("node", ["dist/import-catalog.js"], {
      env: process.env,
      stdio: ["ignore", "pipe", "inherit"],
    });
    let output = "";

    child.stdout.setEncoding("utf8");
    child.stdout.on("data", (chunk: string) => {
      output += chunk;
    });
    child.on("error", reject);
    child.on("close", (code) => {
      if (code !== 0) return reject(new Error(`import exited with ${code}`));
      try {
        resolve(JSON.parse(output) as ImportResult);
      } catch (error) {
        reject(new Error(`invalid import result: ${String(error)}`));
      }
    });
  });
}

async function pingWithRetry(url: string): Promise<void> {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch(url, { method: "POST" });
    if (response.ok) return;

    if (response.status !== 429 || attempt === 2) {
      throw new Error(`heartbeat failed: ${response.status} ${await response.text()}`);
    }

    const retryAfter = Number(response.headers.get("retry-after") ?? "0");
    const delayMs = retryAfter > 0 ? retryAfter * 1_000 : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
}

async function verifyTelemetry(apiKey: string, baseUrl: string): Promise<void> {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch(`${baseUrl}/logs/search`, {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (response.ok) {
      await response.json();
      return;
    }

    if (response.status !== 429 || attempt === 2) {
      throw new Error(`telemetry query failed: ${response.status} ${await response.text()}`);
    }

    const retryAfter = Number(response.headers.get("retry-after") ?? "0");
    const delayMs = retryAfter > 0 ? retryAfter * 1_000 : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
}

async function main(): Promise<void> {
  const heartbeatUrl = process.env.IMPORT_HEARTBEAT_URL;
  if (!heartbeatUrl) throw new Error("IMPORT_HEARTBEAT_URL is required");
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");
  const baseUrl = process.env.INFRAI_BASE_URL;
  if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");

  const runId = randomUUID();
  const startedAt = Date.now();
  const result = await runImport();

  if (result.accepted === 0) throw new Error("import produced no accepted rows");

  console.log(JSON.stringify({
    event: "catalog_import_completed",
    runId,
    durationMs: Date.now() - startedAt,
    ...result,
  }));

  await pingWithRetry(heartbeatUrl);
  await verifyTelemetry(apiKey, baseUrl);
}

main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

There is one deliberate trade-off: a successful commit followed by a failed heartbeat reports the run as missing. That is preferable to acknowledging work before it exists, but the run ID should let an operator reconcile the event. Keep the scheduler's retry policy idempotent so rerunning an import does not duplicate inventory changes. The telemetry query is a wiring check, not alerting; the external heartbeat remains the dead-man switch.

Benchmark the path you actually depend on. Measure check-in latency, alert latency, and status-page update latency separately with a test job. Do not publish invented service-level numbers. Five minutes of setup means little if a delayed notification turns a 15-minute freshness target into a guess.

What I would change at 100 scheduled imports

At 100 imports, hand-created monitors become config debt. Generate heartbeat checks from the same registry that defines store, region, owner, schedule, and freshness objective. Keep that registry vendor-neutral. It should also drive monitor names and status-page component mapping, so a rename cannot strand an alert.

I would add three controls. First, alert on both a missing completion and an implausible result count; a job that “succeeds” with one row can be as harmful as no run. Second, route internal alerts by owning team while aggregating public incidents by customer-visible capability. Third, attach cost dimensions at emission time: storeId, region, environment, and jobName. Retrofitting them later is painful.

Short version: labels first.

Configuration compounds fast. One hundred jobs across two environments already imply 200 schedule identities before regional probes or public components enter the count, so generation and review matter more than another settings screen. The concrete trade-off is central governance versus local clarity: a registry prevents drift, while per-job definitions make ownership obvious beside the worker code. Generate from the registry, but keep the generated monitor reference next to each job.

Keep sensitive customer data out of monitoring payloads. If logs contain user-linked data, verify deletion and export requirements before adopting any store; a system without per-user log deletion or bulk export is a poor fit for workflows that require those controls. Also do not assume trace IDs in logs provide a distributed trace or span tree. They provide correlation, nothing more.

The decision rule

Choose the smallest combination that owns all three outcomes: external detection, dead-man detection, and customer communication. For a small e-commerce app, that often means one hosted uptime/status product plus Healthchecks.io-style job heartbeats. Add a separate telemetry store when incident diagnosis, dependency error grouping, or per-store metrics justify it.

Do not buy overlap without assigning ownership. One service must own the page, one signal must prove each import completed, and one routing path must wake the right person. Test US and EU checks from the regions customers use, then run a controlled missed import and time the notification. The stopwatch settles more arguments than a feature grid.

Sources

Top comments (0)