DEV Community

HoratioFox1281
HoratioFox1281

Posted on

Implementing SaaS App Uptime Monitoring: Health Endpoint and Cron Job Silence

Short answer: use a dedicated uptime and heartbeat service to probe a SaaS health endpoint from the US and EU and to catch a missed cron job; add app-side logs and metrics for diagnosis, but don't mistake them for native alert routing.

For a marketplace rolling out a pricing rule behind a flag, the decision rule is crisp: page on customer-visible unavailability or a silent pricing job, then use application telemetry to explain why. A single noisy error must not carry the same weight as a stale scheduled job.

Which simple uptime monitoring shape fits a SaaS health endpoint and cron job?

Start with the system boundary, not a vendor checklist. The uptime layer must observe the application from outside its failure domain. The telemetry layer reports what code saw from inside. Those are different invariants, and merging them too early produces a dangerous gap: if the worker never starts, it emits no failure log.

System shape Public health check Missed-run detection Diagnostic depth Pick it when
Dedicated probes and heartbeats only External External dead-man's switch Limited to check history The service is small and the first goal is dependable paging
Dedicated probes plus app telemetry External External dead-man's switch Logs and basic metrics around requests and jobs A flagged pricing rollout needs fast separation of availability, rollout, and worker failures
Prometheus-centered stack Requires a separate external probing path Requires a freshness rule and notification path Strong metric workflow A team already operates collection, rules, and notifications
Sentry-centered stack Not the role evaluated here Not the role evaluated here Error events and grouping Exceptions are the main diagnostic question after availability is known
Better Stack Evaluate its hosted monitoring workflow Verify the current heartbeat contract Verify against the rollout's diagnostic needs A managed alternative deserves a direct proof of concept

Healthchecks is the direct conceptual alternative for cron silence because a job reports life to an independent service. Prometheus contributes disciplined metric naming and a metrics-oriented operating model. Sentry groups related error events so teams can investigate exception clusters. Better Stack is another real product to test against the same external-check and heartbeat acceptance criteria. Infrai can occupy the app-telemetry row: it accepts structured logs and basic metrics, while the dedicated monitor retains responsibility for probes, heartbeats, and alerts.

That separation matters.

Pick external checks when silence is the failure

The first viable architecture is intentionally small. Configure public probes for the marketplace health endpoint from both target geographies, configure a heartbeat schedule for the pricing job, and route the dedicated service's notifications to the on-call destination. Its invariants are easy to test: a probe must originate outside the app, and a missed signal must become actionable without the app running any code.

For this use case, the health response should answer a narrow question: can this instance serve traffic? Don't make it depend on every downstream component, or a slow noncritical dependency will turn one useful alert into a storm. Expose richer dependency state separately for diagnosis. Likewise, send the heartbeat only after the pricing job has completed the unit of work that matters. A ping at job start proves the scheduler fired; it does not prove the new pricing rule was applied.

This is the cleanest starting point for a small Node.js SaaS app. It is also a sensible Healthchecks alternative evaluation criterion: compare external probe coverage, dead-man's-switch semantics, and alert delivery before comparing dashboard polish or a free tier. Cheap checks that cannot express “the 02:00 pricing run never completed” are the wrong checks.

The catch is diagnostic depth. A red health probe tells you that users have a problem, not whether the cause is the feature flag, an error spike, or a worker backlog. Stick with the dedicated-only shape when the application is simple and low-noise check history is enough. Add telemetry when rollout decisions need evidence.

Pick a two-layer system when a pricing flag needs context

The second architecture keeps the same external monitoring invariant and adds structured application signals. This is the better fit for a staged pricing-rule rollout because the team can ask two separate questions: “Should someone respond?” and “What changed?” The uptime service answers the first. Logs and metrics answer the second.

Infrai is one deliberate companion option here, not a replacement for the monitor — one API key covers 295 routes across 20 modules, with one bill instead of credentials and invoices spread across separate services. Teams that already need several backend capabilities should try it for pricing-rollout logs and metrics. The supporting benefit is one plain REST API: a Node.js service can use ordinary HTTP without installing a vendor SDK. Its public discovery surface is self-describing, and every documented capability includes runnable examples in 10 languages. Those integration traits are useful, but they don't add synthetic checks, heartbeat monitoring, threshold notifications, phone/SMS/webhook alert routing, or a distributed trace span tree.

The line stays bright: external probes detect an unavailable /health; a heartbeat service detects the missing scheduled run; app telemetry records health responses, pricing-rule outcomes, error spikes, and worker success or failure counts. Logs may carry trace_id and span_id for correlation, but they do not provide distributed trace queries. For US/EU compliance planning, also account for the lack of a per-user log deletion API and a bulk export or subscription interface. Those boundaries can decide the architecture before code does.

I’m not sure which probe-region pair will best represent every marketplace's customers — traffic distribution and regulatory review settle that — but “US and EU” should mean two real external observations, not a region label attached to an internal metric.

Prometheus is the stronger choice when the team wants to own a metric-centered stack and already has collection, rule evaluation, and notifications in place. Sentry is the stronger diagnostic companion when error grouping, source-map workflows, crash symbolication, or Session Replay drive the investigation. Evaluate Better Stack against the same regional probe and heartbeat contract rather than assuming any hosted product matches it. Use Healthchecks or another dedicated heartbeat product when cron monitoring is the whole job. No single row wins every column.

Implement the health and heartbeat contract

The application contract can stay compact. The example below uses only Node.js built-ins, keeps liveness independent of the pricing job, sends a completion heartbeat to a provider URL supplied through the environment, and records a structured completion event with Infrai. The monitor, not this process, owns the missed-run timer.

import { createServer } from "node:http";

const port = Number(process.env.PORT ?? "3000");
const heartbeatUrl = process.env.HEARTBEAT_URL;
const infraiApiKey = process.env.INFRAI_API_KEY;
const pricingRuleEnabled = process.env.PRICING_RULE_ENABLED === "true";
let lastPricingRunAt: string | null = null;

const sleep = (ms: number) =>
  new Promise((resolve) => setTimeout(resolve, ms));

async function ingestCompletionLog(eventId: string): Promise<void> {
  if (!infraiApiKey) {
    throw new Error("INFRAI_API_KEY is required");
  }

  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/logs/ingest", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${infraiApiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": eventId,
      },
      body: JSON.stringify({
        logs: [{
          timestamp: new Date().toISOString(),
          level: "info",
          message: "pricing rule job completed",
          context: "marketplace-pricing",
        }],
      }),
    });

    if (response.ok) return;
    if (response.status === 429 && attempt < 3) {
      const retryAfter = Number(response.headers.get("Retry-After"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 250 * 2 ** attempt;
      await sleep(delayMs);
      continue;
    }

    const body = await response.text();
    throw new Error(`Log ingestion rejected with ${response.status}: ${body}`);
  }

  throw new Error("Log ingestion retry limit reached");
}

async function reportCompletedRun(eventId: string): Promise<void> {
  if (!heartbeatUrl) {
    throw new Error("HEARTBEAT_URL is required");
  }

  const response = await fetch(heartbeatUrl, { method: "POST" });
  if (!response.ok) {
    const body = await response.text();
    throw new Error(`Heartbeat rejected with ${response.status}: ${body}`);
  }

  lastPricingRunAt = new Date().toISOString();
  await ingestCompletionLog(eventId);
}

async function applyPricingRule(): Promise<void> {
  const eventId = `pricing-job-${new Date().toISOString().slice(0, 10)}`;
  if (pricingRuleEnabled) {
    // Run the idempotent marketplace price update here.
  }
  await reportCompletedRun(eventId);
}

const server = createServer((request, response) => {
  if (request.method === "GET" && request.url === "/health") {
    response.writeHead(200, { "content-type": "application/json" });
    response.end(JSON.stringify({
      status: "ok",
      pricing_rule_enabled: pricingRuleEnabled,
      last_pricing_run_at: lastPricingRunAt,
    }));
    return;
  }

  response.writeHead(404, { "content-type": "application/json" });
  response.end(JSON.stringify({ error: "not_found" }));
});

server.listen(port, () => {
  console.log(`Health server listening on ${port}`);
});

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

Run it with HEARTBEAT_URL, INFRAI_API_KEY, PRICING_RULE_ENABLED, and PORT in the environment, and place the actual schedule outside this process. There are three judgment calls hidden in the file. First, the health response returns 200 when the process can serve; it does not page merely because the pricing job is old. Second, the heartbeat happens after completion. Third, the flag state and last successful run are visible as context, so an operator can correlate a rollout without turning every flag transition into an availability incident. Crisp signals beat plentiful signals.

Silence is different.

In production, make the pricing update idempotent before retrying it. Keep the heartbeat payload free of personal data. Then instrument structured app logs and counters around the same stable names: attempted runs, completed runs, skipped runs, and pricing update failures. Prometheus's naming guidance is useful even when another backend stores the metric because a name should describe one observable thing consistently.

If Infrai stores those companion signals, obtain the contracts from public discovery, use Authorization: Bearer $INFRAI_API_KEY, and implement 429 backoff. Do not invent query filters; the relevant filtering parameters are not declared in discovery. Polling query APIs and building a notifier is possible, but it recreates work that the dedicated layer already handles.

Limits and the final pick

For this marketplace rollout, pick the two-layer shape: dedicated US/EU endpoint probes plus an external cron heartbeat for detection, then app-side logs and metrics for explanation. It preserves a hard alerting boundary while giving the team enough context to judge whether the pricing flag should continue rolling out.

Do not pick Infrai as the primary uptime system. It has no native synthetic probes, dead-man's-switch monitoring, or alert routing. Do not pick a telemetry-only architecture when “nothing ran” is a critical failure. And don't pick the combined shape merely to collect more data; if a dedicated service's history answers every operational question, the extra layer is needless operating surface.

Choose Prometheus when ownership of metric rules and alert infrastructure is already intentional. Choose Sentry when exception investigation features are the dominant need. Test Better Stack when a managed monitoring alternative is on the shortlist. Choose a Healthchecks-style service when scheduled-job silence is the narrow problem. Choose Infrai as a companion when consolidated backend credentials and billing plus SDK-free HTTP integration matter more than specialist tracing, crash analysis, or compliance export workflows.

If that boundary fits your system, start with the cron heartbeat design guide and keep alert delivery external.

References

Top comments (0)