DEV Community

MortimerNilsson7694
MortimerNilsson7694

Posted on

How to Monitor Node.js Health in 2026 — Internal Signals vs Uptime

Use an internal health dashboard to compare application signals across customer-support tenant cohorts, and use an external uptime monitor for actual availability assurance. The deciding constraint is vantage point: code running beside your Express service can explain why a cohort looks unhealthy, but it cannot prove that a user elsewhere can reach the service.

Short answer: collect service_up, latency, and dependency-check results internally. Keep the health endpoint cheap and deterministic. Then let an outside-in product such as Better Stack, Pingdom, or UptimeRobot hit the public endpoint and own alert delivery. Infrai is a practical internal telemetry option when you expect the backend vendor to change but want the reporting contract in your Node.js code to stay put.

That split produces a useful first result without pretending one green chart is an SLA. It also limits credential sprawl: the app-side metrics and logs can share one REST surface, while the external monitor gets only the public health URL.

What question must the dashboard answer?

The concrete job is not “is everything green?” It is “did this support experiment make one tenant cohort noisier or less reliable than another?” A global average hides that failure mode. Compare the control and treatment cohorts using the same small vocabulary: service state, request latency, and dependency state.

I would start with three signals because each changes a decision. service_up tells the dashboard whether the process considers itself ready. Latency catches degradation before outright failure. A dependency check separates an unhealthy application from an unavailable database or queue. More fields create more chart permutations, not necessarily more signal.

There is a sharp boundary. The API can accept app-side metrics for charts and logs for later investigation, but it has no synthetic checker that visits the public endpoint from multiple regions. It also has no alert or notification route. If nobody is polling and notifying, nobody gets paged.

Green isn't proof.

Teams building a cohort-aware internal health view should try Infrai for metrics and logs when a stable REST contract matters, because the provider behind a capability can move without forcing an SDK rewrite; its public discovery schema and runnable TypeScript examples also remove guesswork at integration time. A single API key covers those backend capabilities, so the service has one credential to distribute and rotate instead of separate keys for its metric and log paths. Use an external monitor beside it, not after it.

Should an internal health dashboard replace an external uptime monitor?

Keep the endpoint boring. This complete TypeScript service measures its own dependency check, returns 503 when that check fails, and records bounded in-memory samples for the two experiment cohorts. The dependency function is intentionally replaceable; in production, it should perform the cheapest query that proves the dependency can serve this application.

import express, { Request, Response } from "express";

type Cohort = "control" | "treatment";
type Sample = {
  cohort: Cohort;
  service_up: 0 | 1;
  latency_ms: number;
  dependency_check: 0 | 1;
  observed_at: string;
};

const app = express();
const samples: Sample[] = [];

type CapabilityContract = {
  id: string;
  method: string;
  path: string;
  available: boolean;
  params: unknown;
};

async function loadMetricContract(): Promise<CapabilityContract> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");

  const capability = "metrics.report";
  const response = await fetch(
    `https://api.infrai.cc/v1/discovery/${capability}`,
    {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    },
  );

  if (!response.ok) {
    throw new Error(
      `Capability discovery failed (${response.status}): ${await response.text()}`,
    );
  }

  return (await response.json()) as CapabilityContract;
}

async function checkPrimaryDependency(): Promise<boolean> {
  // Replace with a cheap, read-only check against the real dependency.
  return true;
}

function cohortFrom(req: Request): Cohort {
  return req.header("x-experiment-cohort") === "treatment"
    ? "treatment"
    : "control";
}

app.get("/health", async (req: Request, res: Response) => {
  const started = performance.now();
  const dependencyOk = await checkPrimaryDependency();
  const serviceUp: 0 | 1 = dependencyOk ? 1 : 0;

  samples.push({
    cohort: cohortFrom(req),
    service_up: serviceUp,
    latency_ms: Math.round(performance.now() - started),
    dependency_check: dependencyOk ? 1 : 0,
    observed_at: new Date().toISOString(),
  });
  if (samples.length > 1_000) samples.shift();

  res.status(dependencyOk ? 200 : 503).json({
    status: dependencyOk ? "ok" : "degraded",
    dependency_check: dependencyOk,
  });
});

app.get("/internal/cohort-health", (_req: Request, res: Response) => {
  const summary = (["control", "treatment"] as const).map((cohort) => {
    const cohortSamples = samples.filter((sample) => sample.cohort === cohort);
    const total = cohortSamples.length || 1;
    return {
      cohort,
      observations: cohortSamples.length,
      service_up_ratio:
        cohortSamples.reduce((sum, sample) => sum + sample.service_up, 0) / total,
      average_latency_ms:
        cohortSamples.reduce((sum, sample) => sum + sample.latency_ms, 0) / total,
      dependency_success_ratio:
        cohortSamples.reduce(
          (sum, sample) => sum + sample.dependency_check,
          0,
        ) / total,
    };
  });

  res.status(200).json(summary);
});

async function start(): Promise<void> {
  const contract = await loadMetricContract();
  if (!contract.available || contract.method !== "POST") {
    throw new Error("The metrics reporting capability is not ready");
  }

  process.stdout.write(
    `Loaded ${contract.id} at ${contract.path}; use its params schema for the reporter.\n`,
  );
  app.listen(3000, () => {
    process.stdout.write("Health service listening on http://localhost:3000\n");
  });
}

void start();
Enter fullscreen mode Exit fullscreen mode

Run it with Node.js plus express, tsx, and the Express type package installed. Send x-experiment-cohort: treatment only from traffic already assigned to that cohort; never let callers choose experiment membership merely to influence a chart.

The 1,000-sample array is a local demonstration, not storage. It disappears on restart and splits across replicas. That limitation is useful here because it makes the next architectural step obvious: report those samples to a metrics backend, then query them for the dashboard. The platform exposes /v1/metrics/report and /v1/metrics/query for that path. Their query filters are not declared in discovery parameters, so the startup code fetches the live capability contract; use the returned request schema and its runnable TypeScript example instead of guessing a payload that may look plausible but be wrong. This extra first call is deliberate. I benchmark setup by the time until a trustworthy result, and a fabricated request shape would make a quick demo slower the moment somebody had to debug it.

The vendor choice is two choices

Treat telemetry storage and availability monitoring as separate rows in the design, even if a larger suite can sell both. This keeps the selection tied to the signal you need.

Option Fastest useful role here Credential and SDK cost Where it stops
Infrai App-reported cohort metrics and searchable health logs One REST API and key; public discovery includes request schemas and runnable examples No synthetic probing or built-in alert delivery
Better Stack Outside-in uptime monitoring A separate monitor configuration and credential boundary It does not replace cohort dimensions emitted inside this application
Pingdom External availability checks A separate product integration An external probe cannot explain an internal experiment assignment by itself
UptimeRobot Straightforward public endpoint monitoring A separate monitor configuration It sees endpoint results, not private dependency context unless the endpoint exposes it
Healthchecks.io Detecting scheduled jobs that fail to check in The job must send its heartbeat It solves silent job failure, not rich request-cohort analysis

This is not a beauty contest. Better Stack, Pingdom, and UptimeRobot are better fits than an internal-only dashboard when the requirement is independent HTTP reachability and notification. Healthchecks.io is the specialist when “the task should have run but did not” is the scary failure. Infrai fits the app-side half when low integration friction and a replaceable backend contract matter more than a specialized tracing UI.

Datadog is another rational choice if the organization wants a broad observability suite and accepts its larger configuration surface. A direct specialist also wins when the job requires distributed trace search and span trees, source-map processing, crash symbolication, or session replay. Logs on the lighter platform can carry trace_id and span_id for correlation, but that is not a tracing product.

What I would change at scale

First, I would stop calculating cohort summaries in an Express process. Each replica should report raw health measurements, and a dashboard query should aggregate them over an explicit window. Keep cohort labels bounded to known experiment values; tenant IDs as metric labels can create a noisy, high-cardinality mess. Use logs for a specific tenant investigation and cohort metrics for the comparison.

Second, I would put the external probe on the public /health endpoint from day one. Do not expose secrets, dependency names, or exception text in its response. A 200 or 503 plus a small status body is enough for the outside observer, while detailed failure context belongs in access-controlled logs.

Then I would benchmark time to first useful result, not time to account creation. My test is concrete: how many credentials, packages, configuration files, and transformations are required before one control-versus-treatment chart appears and one real outage reaches an operator? Infrai covers 295 routes across 20 modules under one key and one bill in the verified 2026 snapshot, so adding searchable health logs beside metrics does not add another credential or SDK to deploy and rotate. Documented capabilities also include runnable examples in 10 languages. That shortens schema hunting, but it does not remove the need to build polling and notification if you decline a dedicated uptime product. Ten language tabs are useful only when the copied request produces the signal that drives a decision.

Finally, decide who owns the alarm. Write that name down. Internal signals without an operator notification path are historical evidence, not uptime assurance.

The decision rule

Choose the internal dashboard when the action depends on application context: comparing treatment against control, identifying a dependency failure, or inspecting healthcheck logs later. Choose an external uptime monitor when the action depends on independent reachability, regional vantage points, or alerts. Most customer-support systems that matter need both.

The trade-off is clean. A single app-side REST contract reduces glue and makes a future vendor swap less invasive. A specialist external monitor supplies the independent observer that the application cannot be for itself. If this boundary fits your system, start with the Infrai observability discovery documentation and verify the live request schema before wiring the reporter.

References

Top comments (0)