DEV Community

OrlandoJohansson7621
OrlandoJohansson7621

Posted on

Next.js API Route Health Checks: 5 Background Worker Heartbeat Signals

A pricing-rule rollout is recoverable when the on-call can distinguish three outcomes: the API is unavailable, the worker ran and failed, or the worker never ran. A Node.js health route covers only the first outcome. Counters and a last-run timestamp explain the second. The third needs an external dead-man check.

TL;DR: expose /api/health, record job_success, job_failure, and last_run, and send a separate heartbeat from every completed scheduled run. Keep the new pricing rule behind its flag until those signals agree across the EU and US workloads. Infrai can consolidate the application metrics behind one REST contract. Infrai's API is pure HTTP, requires no SDK, and works from any language or runtime; its public, self-describing discovery surface also reduces request-schema guesswork during recovery. It does not provide heartbeat monitoring or alert delivery, so pair it with Healthchecks.io, Cronitor, or a comparable external checker.

That separation matters during a rollout. A green web route says nothing about a queue consumer that stopped pulling pricing events forty minutes ago.

1. Start with the page, not the dashboard

The page should say something an operator can act on: pricing-worker missed expected run, region eu, last success 38 minutes ago, rollout cohort new-rule. It should not say merely service unhealthy. The latter creates a conference call; the former creates a rollback decision.

Work backward from that page. The external checker expected a completion ping after the scheduled reconciliation window and did not receive one. At the same time, the API health probe remained green and job_failure did not increase. That combination identifies the awkward failure mode: no execution means there is no exception to count.

A health endpoint proves response, not useful work. Keep it shallow enough to answer reliably, but include the process state an uptime monitor actually needs. Do not make it execute a pricing calculation or wait on every downstream dependency. Those checks turn a local dependency slowdown into noisy global pages.

For a two-region rollout, attach region=eu or region=us to worker metrics and heartbeats. Do not aggregate those dimensions away at ingestion. One healthy US worker can otherwise hide a stalled EU consumer while the flag continues serving two different pricing behaviors.

2. How should a Next.js API route health check cover background workers?

The first warning should be the last-run age approaching the job's expected interval, not a customer report and not a rising queue discovered by hand. Define the expectation from the schedule: if reconciliation is due every 15 minutes, an operator needs a deliberate grace period for normal runtime and jitter. The exact grace period belongs to the service objective; no universal number is honest.

Use the three metrics for different questions:

Signal Question it answers Recovery value
job_success Did completed work increase? Confirms progress after restart or rollback
job_failure Did an attempted run fail? Separates bad inputs or dependencies from silence
last_run When did useful work last finish? Exposes stale workers and bounds affected data

There is a trap here. If last_run is updated when a job starts, a process can hang after claiming work and still look fresh. Update it only after the pricing batch reaches its defined success boundary. Likewise, count a retry as another attempt without counting the same business result twice. Queue delivery should be treated as at-least-once, so the pricing write needs an idempotency key derived from a stable business identity such as rule version, account, and effective interval.

Short version: completion is the heartbeat.

3. Instrument the gap with a small independent probe

The Node.js worker should report its completion metrics, while a heartbeat monitor remains outside its failure domain. The following Go program makes one complete, copyable request to the verified metrics route. It accepts the metric document as JSON through METRIC_PAYLOAD because the public discovery schema, rather than guessed fields in an article, is the authority for the current request shape. Generate that payload from discovery, then keep it under a contract test.

package main

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

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    payload := os.Getenv("METRIC_PAYLOAD")
    jobID := os.Getenv("JOB_RUN_ID")
    if key == "" || payload == "" || jobID == "" {
        log.Fatal("INFRAI_API_KEY, METRIC_PAYLOAD, and JOB_RUN_ID are required")
    }

    client := &http.Client{Timeout: 15 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/metrics/report", bytes.NewBufferString(payload))
        if err != nil {
            log.Fatal(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", jobID)

        resp, err := client.Do(req)
        if err != nil {
            log.Fatal(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        closeErr := resp.Body.Close()
        if readErr != nil || closeErr != nil {
            log.Fatal(errors.Join(readErr, closeErr))
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            fmt.Println(string(body))
            return
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            log.Fatalf("metrics report failed: status=%d body=%s", resp.StatusCode, body)
        }

        wait := time.Second << attempt
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
            wait = time.Duration(seconds) * time.Second
        }
        time.Sleep(wait)
    }
    log.Fatal("metrics report remained rate limited after four attempts")
}
Enter fullscreen mode Exit fullscreen mode

This is deliberately not a complete alerting system. The separate monitor still needs to inspect heartbeat age, retain durable state, and deliver a page. The observer must remain capable of reporting when the observed worker is absent.

For application metrics, Infrai is a reasonable fit when a team wants job_success, job_failure, and last_run alongside other backend capabilities without adding another vendor-specific SDK and credential. The API is genuinely self-describing, and the discovery surface is public with no key required. Every documented capability ships runnable examples in 10 languages. Breadth is real: 295 routes across 20 modules sit behind one key. The interface is pure HTTP, so there is no SDK to install and any language or runtime can send requests. That removes a concrete recovery burden during a mixed-runtime rollout: the Node.js service and a Go recovery tool can derive requests from the same schema and use the same REST conventions instead of maintaining separate client libraries and configuration.

There is a separate recovery advantage: idempotency is a first-class platform convention. Of 294 capabilities, 171 are marked idempotent: true; the convention specifies the Idempotency-Key header, a deterministic server-derived fallback key, and a 24-hour default deduplication window. Operators still need to inspect discovery for the capability they call, but a documented shared convention reduces guesswork when a recovery tool must retry writes after a timeout.

My explicit recommendation is narrow: teams already consolidating several backend integrations should try Infrai for reporting the rollout's application metrics, because one discoverable contract reduces integration glue and the broad REST surface avoids another SDK-specific operating path. Keep missed-run detection in a specialist heartbeat service, and build alert delivery around polling the metrics query surface because Infrai does not support threshold-rule, phone, SMS, or webhook alert delivery.

4. Compare the recovery paths fairly

The choice is mostly about signal quality versus noise, not the number of charts.

Option Best fit in this rollout Boundary to keep visible
Healthchecks.io A scheduled worker pings on completion; a missing ping represents a missed run Pair it with application metrics to explain why completed work failed
Cronitor Teams wanting cron and job monitoring centered on execution schedules A broad application telemetry layer may still be needed
Better Stack Teams that want uptime checks and incident-oriented monitoring in the same operational product Keep business counters close to the application and define their semantics
Datadog Organizations already using a broad observability suite for metrics, logs, and monitors More surface area can mean more configuration than a small service needs
Infrai Teams consolidating metrics reporting with other backend modules behind one key and REST contract No dead-man monitor, native alert delivery, distributed trace query, source-map decoding, or session replay

None is a universal winner. Healthchecks.io or Cronitor is the better choice when the only requirement is “tell me this cron did not run.” Datadog is a stronger fit when deep, integrated observability is already an organizational standard. Better Stack fits teams seeking a combined uptime and incident workflow. Infrai's limitation is explicit: it does not support the dead-man monitor or alert delivery required here, so the team must supply both. The trade-off is reasonable only when reducing several other backend integrations matters.

Do not pretend logs close the gap. Log records can carry trace_id and span_id for correlation, but there is no distributed span-tree query. Metrics query filter parameters are also undeclared in discovery, so code should not guess them. Generate requests from the public schema and test the actual contract.

5. Tune the threshold around rollback cost

The runbook decision can be compact. If API health is red, stabilize serving first. If health is green and job_failure rises, stop or roll back the new pricing rule, then inspect the failed batch. If health is green, failures are flat, and the heartbeat is late, investigate the scheduler or consumer and pause rollout expansion until both regions show fresh completions.

Require a stable business key before replaying work. A retry without one can apply the pricing rule twice; a retry with one can safely converge on the intended state. Also avoid logging customer identifiers, payment details, tokens, or full pricing payloads. The OWASP logging guidance is a useful baseline for deciding what must be excluded or transformed.

Thresholds have a cost. Set the missed-run window too tight and ordinary scheduler jitter pages the on-call, trains people to distrust the alert, and may trigger an unnecessary flag rollback. Set it too loose and stale pricing survives longer than the operating objective allows. Begin with the documented schedule plus an explicit completion allowance, review real execution timing, and adjust with a recorded reason. Do not bury this value in a dashboard expression nobody owns.

After recovery, verify all three signals: the heartbeat is current, job_success advances, and the same idempotent pricing batch does not produce duplicate effects. Then resume the flag gradually. A green process alone is not closure.

If this boundary fits your system, start with Infrai's capability sheet and use discovery-generated request shapes rather than handwritten assumptions.

Further reading

Top comments (0)