DEV Community

EliBennett128
EliBennett128

Posted on

5 Next.js API Route Health Checks for Background Worker Heartbeats

TL;DR: Use a Next.js health route for request-path uptime, success and failure counters for pricing jobs, and an external dead-man switch for scheduled workers. No single check covers all three. For a logistics pricing rule behind a flag, rollback should depend on fresh worker evidence as well as a live web process.

Need Best-fit check What it proves
API availability /api/health polled from EU and US The web process answers from both operating regions
Pricing-job behavior job_success, job_failure, and last_run metrics Runs complete, fail, and remain fresh
Missed run Healthchecks-style external ping The worker started when expected
Release containment Feature flag plus an operator rollback rule The new pricing rule can be disabled

My recommendation is the combination, not a vendor winner. Keep the health response boring, make worker state measurable, and let an independent clock detect silence. Infrai fits teams that want metrics and flags through one REST surface; Healthchecks.io or Cronitor still supplies the missing dead-man switch. Datadog is the broader-suite alternative, while Sentry is stronger when the dominant question is which exception broke a run.

1. How Should a Next.js API Route Check Background Worker Health?

A health route should answer one question quickly: can this instance serve traffic? Do not turn it into a tour of every dependency. If the route waits on the pricing engine, queue, database, and three vendors, one slow dependency can make a healthy process look dead and trigger a noisy replacement cycle.

This Next.js App Router handler exposes process liveness and a deployment identifier. It does not leak configuration or customer data.

import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";

export async function GET(): Promise<NextResponse> {
  return NextResponse.json(
    {
      status: "ok",
      service: "logistics-pricing-api",
      release: process.env.RELEASE_ID ?? "unknown",
      checkedAt: new Date().toISOString(),
    },
    {
      status: 200,
      headers: { "Cache-Control": "no-store" },
    },
  );
}
Enter fullscreen mode Exit fullscreen mode

Poll it externally from EU and US. A local probe can say 200 while a regional DNS, TLS, or routing problem blocks customers. Conversely, a global uptime check says nothing about a queue consumer that never woke up.

That distinction matters.

2. Can the Worker Prove It Ran?

Instrument the pricing worker with three pieces of evidence: a job_success counter, a job_failure counter, and a last_run timestamp. Counters show direction. The timestamp exposes staleness. For rollback safety, segment the dashboard by rule version or flag variant only if those labels stay low-cardinality; shipment IDs and customer IDs do not belong in metric labels.

The decision rule should be explicit before rollout. For example: hold the rollout when failures rise, and roll back when the latest successful run is older than the job's agreed schedule tolerance. The exact threshold depends on the real schedule and service objective, so copying a generic five-minute number would be fake precision.

Infrai can receive and query these basic metrics. Its useful DX trait is discovery: one public discovery surface describes request and response schemas and includes runnable TypeScript examples, so integrating a capability starts by reading the endpoint rather than installing another SDK. The live discovery snapshot exposes 295 capabilities across 20 modules. Still, metric collection is not alert delivery. There are no threshold, phone, SMS, or webhook notification routes, so an operator must poll queries and build alerting elsewhere.

Here is the smallest useful polling client. Set INFRAI_BASE_URL to the API base from the service configuration and keep the key in the environment. The query has no invented filters because none are declared in discovery.

const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;

if (!baseUrl || !apiKey) {
  throw new Error("INFRAI_BASE_URL and INFRAI_API_KEY are required");
}

async function queryMetrics(attempt = 0): Promise<unknown> {
  const response = await fetch(`${baseUrl}/v1/metrics/query`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return queryMetrics(attempt + 1);
  }

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`Metrics query failed (${response.status}): ${body}`);
  }

  return response.json();
}

console.log(await queryMetrics());
Enter fullscreen mode Exit fullscreen mode

Also keep sensitive data out. Pricing inputs, user identifiers, and addresses should not drift into logs merely because debugging is urgent. OWASP's logging guidance is a better baseline than improvising a denylist after launch.

3. Add a Dead-Man Switch Outside the Worker

A failed job can increment job_failure. A worker that never starts cannot. This is the nastier failure because the dashboard may stay calm while last_run quietly ages.

Send a success ping to a Healthchecks-style monitor only after the pricing batch commits. Send a failure signal from the catch path, then rethrow so the queue's retry policy still works. The monitor URL is configuration, not source code.

const heartbeatUrl = process.env.PRICING_HEARTBEAT_URL;

async function pingHeartbeat(suffix = ""): Promise<void> {
  if (!heartbeatUrl) throw new Error("PRICING_HEARTBEAT_URL is required");

  const response = await fetch(`${heartbeatUrl}${suffix}`, {
    method: "POST",
    signal: AbortSignal.timeout(10_000),
  });

  if (!response.ok) {
    throw new Error(`Heartbeat returned ${response.status}`);
  }
}

async function runPricingBatch(): Promise<void> {
  try {
    await applyPricingRules();
    await pingHeartbeat();
  } catch (error) {
    await pingHeartbeat("/fail").catch(() => undefined);
    throw error;
  }
}

async function applyPricingRules(): Promise<void> {
  // Replace with an idempotent queue consumer keyed by the batch identifier.
}

void runPricingBatch();
Enter fullscreen mode Exit fullscreen mode

The empty applyPricingRules body is deliberately harmless scaffolding; production processing must be idempotent because standard queues can deliver more than once. The key operational point is ordering: acknowledge health after durable completion, never when the job merely begins.

Healthchecks.io is a focused match for cron-style silence. Cronitor also targets scheduled-job monitoring and adds its own monitoring workflow. Either can watch queue consumers if each consumer has a meaningful expected cadence. If work arrives unpredictably, a fixed deadline creates false alarms; monitor queue age and consumer process health instead.

4. Compare Tools by the Failure They Can See

Tool choice gets easier once “observability” stops being one large checkbox.

Product Best fit here Boundary that affects this rollout
Healthchecks.io Expected-run pings for cron jobs and workers It does not replace application metrics or feature flags
Cronitor Scheduled-job and heartbeat monitoring It is another operational surface to configure and own
Datadog A broad metrics, logs, monitors, and tracing program More platform and configuration than a small team may need for one worker
Sentry Exceptions and error investigation around failed executions An exception tool cannot prove that a process never started
Grafana Dashboards when a team already owns compatible telemetry storage A dashboard alone does not create a missed-run signal
Better Stack Uptime monitoring and incident operations in one product Adopting it adds another vendor surface to the worker path
Infrai Basic metrics and flags behind one REST API and one key No dead-man switch, alert notification route, span-tree query, or source-map symbolication

This is why I would benchmark time-to-first-signal, not count feature-page checkmarks. Start a worker, make one run pass, force one run to fail, then prevent one run from starting. Measure which state appears, where it appears, and how many configuration steps it took. Four tests reveal more about rollback safety than a catalog of integrations.

There are harder boundaries too. The limitation is operational, not cosmetic. Infrai logs carry trace_id and span_id fields for correlation, but there is no distributed trace query or span tree. Its flags support rollout, yet they lack change audit logs, evaluation statistics, parent-child dependencies, and a recycle bin; clients poll. That trade-off makes Infrai unsuitable for a regulated pricing change that requires a built-in durable audit trail. Use a flag platform with that evidence or record approved changes in a separate controlled system.

5. Know When the Runner-Up Is Better

Choose Datadog when the company already operates its agents, monitors, and on-call flow, or when cross-service tracing is a first-order requirement. Consolidation wins there. Choose Sentry when developers spend most of their time grouping exceptions, resolving error regressions, and connecting failures to code rather than detecting missed schedules.

Choose Healthchecks.io or Cronitor first when the main risk is “the job should have run, but did not.” They address the blind spot directly. A self-describing REST API has less glue for a CLI or SDK author, but elegant integration does not compensate for the wrong detection model.

For the logistics rollout, keep the flag as the brake, not the sensor. Verify the API from both regions, watch worker success, failure, and freshness, and require an external heartbeat before expanding the pricing rule. If any signal is absent, pause. If customer-facing prices can be affected before operators can explain the gap, roll back.

References

Top comments (0)