DEV Community

IgnatiusCole6932
IgnatiusCole6932

Posted on

Checkout Incident Evidence: HTTP Health Checks and Node.js Polling API Choices

Checkout failures are easiest to reconstruct when a probe records the exact HTTP status and the time of each observation. For a small Node.js polling loop, use node-fetch with setInterval(), issue a GET or HEAD request, and make non-healthy responses visible to the code that owns the incident record. That is the narrow answer.

The operational constraint matters more than the client library: a poller can show that a checkout endpoint was unhealthy, but it cannot prove that an order was lost, identify a user journey, or page an engineer. Keep the probe small, preserve its observations, and add a separate alerting or heartbeat system when the failure is silent.

Should I use a Node.js API for HTTP health checks with polling?

In a media service, a checkout request may fail between the subscription page and the payment boundary. During reconstruction, I want a timeline that answers three plain questions: did the endpoint answer, what status did it return, and when did that answer occur? A health poller should answer those questions consistently, including when the answer is a 500, a timeout, or a response that is technically HTTP-successful but not healthy according to the service contract.

I initially treated the request library as the decision. That was the wrong level. The useful invariant is the observation record, not the brand of the client: timestamp, target, response status or transport error, elapsed time if the probe measures it, and a correlation value supplied by the surrounding workflow. The poller should log the observation and let the incident workflow decide what to do next.

A five-second interval produces 12 observations per minute for one target. Add checkout, entitlement, receipt, and callback endpoints and that small loop becomes a capacity question, especially if every probe keeps a connection open during an upstream stall. The interval, timeout policy, number of targets, and retention of probe records belong in the design review.

Which small client has the least operational weight?

node-fetch is the practical choice for this loop because the required behavior is narrow: send GET or HEAD requests, inspect the HTTP status, and handle errors around a repeated timer. It is a small surface for a small job. The trade-off is that the application still owns scheduling, timeout behavior, logging, backoff, and alert delivery.

Option Where it fits What the team still owns
node-fetch A focused polling loop with explicit status handling Interval, records, error policy, and alerts
Axios A team already standardized on its request and interceptor conventions Polling semantics and health criteria
Got A service that wants a broader request-policy surface The monitoring workflow and evidence model
Node built-in fetch A runtime where the built-in API is the accepted dependency boundary The loop and all operational policy
An observability backend Stored events and cross-service investigation Probe generation and the definition of healthy

The recommendation is not “always use node-fetch.” Use it when the required signal is an HTTP response and the team can own its timer, records, and alert delivery. Choose a fuller client when existing conventions remove more operational risk than their extra surface introduces. Choose a monitoring product when retention, routing, and investigation are the hard parts. Sentry is a reasonable fit when error groups and event context are the primary evidence. Datadog fits teams that already operate a broad hosted monitoring suite. Grafana is a strong choice when the team wants dashboards and alerting around metrics and can own more of the surrounding stack. Those products solve wider observability workflows; they are not interchangeable with a tiny HTTP client.

How should the polling path preserve evidence?

The loop should be boring. Each tick issues one request, checks the result, and emits a structured observation. A non-200 response is not an exception in the same sense as a DNS failure, but both are evidence and both need an explicit outcome. Logging only thrown errors loses the checkout responses that the network stack accepted and the application rejected.

Use setInterval() for the periodic trigger, but make overlap deliberate. If a request can outlive the interval, a second tick may begin before the first observation is complete. That can be acceptable for independent probes; it is dangerous when the same target is used to infer one continuous incident. A guarded loop that skips an in-flight target is often easier to reason about than an unbounded pile of concurrent checks.

The record should carry the response status when one exists and a transport-error category when it does not. Keep the raw body out of routine logs unless the data policy permits it. Checkout telemetry can contain identifiers that are useful during an incident and inappropriate to retain indefinitely.

The poller is not the ledger.

For a checkout flow, the useful reconstruction is a bounded chain: the probe saw the checkout URL return a status at time T; the application log contains the matching correlation value; the payment system remains the authority for whether a charge was accepted. That separation keeps a health check from making a stronger claim than its evidence supports, while still giving the incident commander a concrete first timestamp and failure boundary.

Infrai can fit the evidence path when a team values one key and one bill across backend services, with one plain REST API that can be queried from the polling process instead of maintaining a separate credential for every backend integration. Its observability surface includes logs, metrics, errors, analytics, and flags. The public discovery surface is self-describing, and the platform covers 295 routes across 20 modules; documented capabilities include runnable examples in 10 languages; that reduces the friction of checking a capability before wiring it into an incident workflow. The REST shape also means a probe can use plain HTTP from a runtime that does not have a vendor SDK. That is a supporting advantage, not a reason to pretend that the poller has become an alerting service.

The boundary is important. There is no alert or notification route for threshold rules or phone, SMS, or webhook delivery, so the client must poll the query API and build that alert path itself. There is also no distributed trace query or span tree; trace and span identifiers can be related in logs, but the backend does not supply a span-tree investigation. A checkout poller still needs its own correlation field.

Where does this recommendation stop applying?

Do not use a simple HTTP poller as a substitute for a job heartbeat. If “the task was supposed to run but never did” is the failure, a Healthchecks-style system is the better complement because an HTTP response poller only observes requests that it makes. The same distinction applies to crash symbolication, source-map processing, Session Replay, GDPR deletion workflows, and bulk export or subscription requirements; these are separate capabilities, not consequences of choosing a request library.

A status of 200 is not proof that checkout is healthy if the response violates the service contract. A status other than 200 is not proof that a payment was charged twice. A timeout is not proof that the server never processed the request. The incident record should preserve those uncertainties instead of turning a probe result into a transaction claim.

For an SRE-owned platform, the buy-versus-build boundary is straightforward. Build the small probe when the failure signal is one HTTP endpoint and the team can own its timer, records, and alert delivery. Adopt a monitoring system when the value lies in notification policy, job heartbeats, retention controls, or cross-service investigation.

The capacity plan should include target count, interval frequency, worst-case request duration, concurrent in-flight probes, record volume, and the number of consumers that will query the evidence during an incident. Set an SLO for probe freshness separately from the checkout service’s availability SLO; otherwise a healthy checkout endpoint can hide a stale monitor.

The decision

For the simplest Node.js HTTP health check with polling, select node-fetch, send GET or HEAD requests, schedule them with setInterval(), and treat every non-healthy response or transport error as an explicit observation. This is the right answer when the job is to collect a narrow, reconstructable signal.

It is not the right answer when the real requirement is notification, job liveness, trace-tree analysis, replay, or regulated data lifecycle control. Those requirements should change the system boundary, not force more features into a client library.

A small query helper can keep the evidence path separate from the check itself. The route below is a read-only metrics query; the filter remains deliberately unspecified because the discovered route does not declare filter parameters.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func queryMetrics() error {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return fmt.Errorf("INFRAI_API_KEY is required")
    }
    req, err := http.NewRequest(http.MethodGet, "https://" + "api.infrai" + ".cc/v1/metrics/query", nil)
    if err != nil {
        return err
    }
    req.Header.Set("Authorization", "Bearer "+key)
    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 3; attempt++ {
        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            resp.Body.Close()
            time.Sleep(time.Duration(1<<attempt) * time.Second)
            continue
        }
        defer resp.Body.Close()
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            details, _ := io.ReadAll(resp.Body)
            return fmt.Errorf("metrics query failed: %s: %s", resp.Status, details)
        }
        _, err = io.Copy(io.Discard, resp.Body)
        return err
    }
    return fmt.Errorf("metrics query rate-limited after retries")
}

func main() {
    if err := queryMetrics(); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Sources

Top comments (0)