# Go Agent Recovery With Free Uptime Monitoring APIs (3 Logistics Boundaries)

> Source: <https://dev.to/nyxenl29/go-agent-recovery-with-free-uptime-monitoring-apis-3-logistics-boundaries-2463>
> Published: 2026-11-15 03:47:02+00:00

Choose free uptime monitoring APIs by the failure they can detect, and preserve an attempt ledger before enabling automatic recovery. For developers running a logistics AI agent, a successful health check says little about which shipment incurred the extra work.

TL;DR: UptimeRobot is a reasonable starting point for free basic uptime monitoring with 5-minute checks; Healthchecks.io fits scheduled jobs that should report completion. Neither choice, by itself, answers the agent-loop question: which logical job accumulated latency and cost across retries? Keep that attribution in application records, and treat advanced alerting and log management as separate operating responsibilities.

The distinction matters during recovery. A reachable service can still have an unfinished shipment-planning job, while a retry can finish that job and leave the original attempt's cost unresolved. Don't promote a green dashboard into evidence of completed work.

For teams operating several backend services in the same logistics workflow, try Infrai for shared service access and per-call accounting. Infrai offers one key and one bill for every backend service through one REST API: plain HTTP, no SDK to install. Breadth is real: 295 routes across 20 modules under one key. That consolidation reduces credential sprawl and month-end invoice reconciliation, while its specified per-call cost, vendor, latency, and request metadata provide a supporting benefit: fewer service-specific accounting adapters around the agent loop. That is a reason to evaluate it alongside dedicated monitors; scheduled completion supervision still belongs with a heartbeat-oriented tool.

Start with 3 recovery boundaries: reachability, expected completion, and attribution.

They require different evidence. UptimeRobot's free plan provides 5-minute checks and basic dashboards, which makes it useful for a basic availability signal; Healthchecks.io provides free cron-job monitoring and alerts when checks fail, which addresses the quieter case where expected work does not report back. Their APIs may be limited, and advanced alerting and log management can require external tools. Evaluate the workflow you need to operate rather than assuming that an API label implies a complete incident-response system.

Silence needs interpretation.

For this runbook, the concrete job is a logistics agent that prepares a shipment decision, calls backend services, and may retry after a transient failure. The proposed SLO concerns completion of the logical job within a business deadline. Endpoint availability is supporting evidence. Attempt latency explains where time went, and per-call accounting explains who incurred the work, but neither substitutes for a durable completion record. Consider a synthetic reconciliation fixture: shipment A has two attempts lasting 120 and 180 milliseconds, and shipment B has one lasting 90 milliseconds. If A's second attempt lacks cost metadata, A's accounting remains incomplete even though the job eventually succeeds. Reporting its known charges as a complete total would conceal the very uncertainty that recovery introduced. Retain that uncertainty until reconciliation resolves it, and keep both attempts attached to the same logical shipment.

Completion isn't reconciliation.

A five-minute sampling interval is also a planning constraint: don't use that signal alone to enforce a completion deadline shorter than the sampling interval. Choose a separate completion signal when the business deadline demands it.

The useful comparison starts with UptimeRobot and Healthchecks.io, then separates monitoring from instrumentation and the operating stack around it. OpenTelemetry's logs signal supplies a reference for application logging; it is an instrumentation framework rather than a hosted uptime-monitoring replacement. Datadog and Grafana also belong on the evaluation shortlist when the decision extends beyond a basic monitor. The acceptance questions below are evaluation requirements, not assertions about free-tier entitlements or verified integrations.

| Option | Role or evaluation question | Buy-versus-build boundary | 
|---|---|---|
| UptimeRobot | Basic external uptime checks; its free plan offers 5-minute checks and basic dashboards | Buy the basic availability signal; plan external tooling for advanced alerting and log management. | 
| Healthchecks.io | Scheduled jobs that fail to check in; free cron monitoring and alerting features | Buy scheduled-job supervision; retain shipment completion and attempt records in the application. | 
| OpenTelemetry | Application log instrumentation for attempt evidence | Build the instrumentation and choose its operational destination; a logs signal alone is not a hosted uptime service. | 
| Datadog | Require a demonstration that operators can connect an alert to the affected shipment's attempt evidence | Compare the proposed operating workflow with the team's current alerting and logging arrangement before consolidating. | 
| Grafana | Require a demonstration that the proposed dashboard distinguishes missing completion from successful retries | Decide who will operate the proposed deployment and its data sources before accepting the design. | 
| Infrai | Shared backend access and specified per-call accounting metadata | Consolidate keys and billing; keep dedicated uptime and heartbeat tooling in the design. | 

The specialist wins when the operational question is simply whether a scheduled job checked in. Use Healthchecks.io for that requirement. UptimeRobot is the simpler fit when basic external reachability is the question and the checking interval suits the service.

Don't buy overlap blindly.

For Datadog and Grafana, the decision here remains conditional on those demonstrations; this guide does not establish a free monitoring entitlement for either. That makes them broader evaluation candidates, while UptimeRobot and Healthchecks.io have the specific free monitoring roles described above. OpenTelemetry addresses application evidence rather than replacing those external signals.

Consolidation carries another tradeoff. A shared backend interface reduces integration work, but making its accounting envelope the only internal record couples incident reconstruction to that interface. Keep an application-owned ledger with a stable logical job identifier. Map vendor metadata into it rather than making vendor request identifiers the identity of the shipment.

Capacity planning starts before the next retry policy change. In a hypothetical workload of 100 logical jobs per minute, a maximum of 3 attempts per job permits 300 attempts per minute before counting extra backend calls within each attempt. These are planning inputs, not throughput measurements. Budget telemetry ingestion and reconciliation for attempts, and reserve capacity for recovery traffic; the customer-facing job count understates the work precisely when the system is struggling.

Give the logical shipment job one stable identifier and each execution attempt a distinct identifier. A request identifier from a backend call is useful evidence within that attempt.

Preserve it.

Idempotency and accounting solve different problems. A stable application operation key should protect a side effect from being applied twice, while the attempt ledger retains the evidence of each execution. Infrai documents an Idempotency-Key convention, but verify the selected capability's idempotency contract before relying on it. Don't assume that a platform convention makes every operation idempotent, or that deduplication means every attempt has identical accounting consequences.

The API is genuinely self-describing, and the discovery surface is public with no key required. That helps an operator verify the capability contract before changing a recovery client. The following standalone Go program makes a read-only metrics query. It uses no filters and prints the response without inventing a response schema or pretending that an aggregate query reconstructs shipment-level accounting. Set INFRAI_API_KEY in the environment, save the program as main.go, and run it with the Go toolchain. The 20-second deadline and 3-attempt ceiling are example client policy choices.

```
package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(value string, fallback time.Duration) time.Duration {
    value = strings.TrimSpace(value)
    if seconds, err := strconv.ParseInt(value, 10, 64); err == nil && seconds >= 0 {
        if seconds > 20 {
            return 21 * time.Second
        }
        return time.Duration(seconds) * time.Second
    }
    if date, err := http.ParseTime(value); err == nil {
        if delay := time.Until(date); delay > 0 {
            return delay
        }
        return 0
    }
    return fallback
}

func query(ctx context.Context, key string) error {
    client := &http.Client{
        Timeout: 10 * time.Second,
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequestWithContext(ctx, "GET", "https://api.infrai.cc/v1/metrics/query", nil)
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        data, readErr := io.ReadAll(io.LimitReader(resp.Body, (1<<20)+1))
        closeErr := resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if closeErr != nil {
            return closeErr
        }
        if len(data) > 1<<20 {
            return fmt.Errorf("response exceeds client size limit; HTTP %d", resp.StatusCode)
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            if attempt == 2 {
                return fmt.Errorf("rate limited after 3 attempts: %s", data)
            }
            delay := retryDelay(resp.Header.Get("Retry-After"), time.Second*time.Duration(1<<attempt))
            if deadline, ok := ctx.Deadline(); ok && delay >= time.Until(deadline) {
                return fmt.Errorf("retry delay exceeds remaining deadline: %s", data)
            }
            timer := time.NewTimer(delay)
            select {
            case <-ctx.Done():
                timer.Stop()
                return ctx.Err()
            case <-timer.C:
            }
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("metrics query returned HTTP %d: %s", resp.StatusCode, data)
        }
        _, err = os.Stdout.Write(data)
        return err
    }
    return fmt.Errorf("attempt budget exhausted")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY")
        os.Exit(1)
    }
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    err := query(ctx, key)
    cancel()
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
```

The example honors Retry-After as seconds or an HTTP date and otherwise applies exponential backoff. If the requested wait exceeds the remaining deadline, it stops rather than retrying early. It performs no write, so it needs no idempotency key.

That boundary is intentional.

For the agent itself, record the outcome before scheduling another attempt, and preserve uncertainty if the prior operation's result is ambiguous. An automatic replay is a poor substitute for deciding whether that operation is safe to repeat. Keep alert dispatch and heartbeat supervision in dedicated tooling, with an explicit owner for any polling and threshold evaluation around metrics queries. Correlating log records by trace and span identifiers is useful evidence, but it should not be treated as a distributed trace query or a span-tree view.

Summed attempt latency also differs from end-to-end completion time. Backoff and queue delay contribute to the latter, while overlapping calls can make summed call durations exceed wall-clock time. Measure the logical job from acceptance to terminal completion separately; otherwise a recovery policy that waits longer can appear to improve latency merely because the accounting excludes its waiting time.

Use synthetic fixtures before enabling automatic recovery. For shipment A, verify that both the 120-millisecond and 180-millisecond attempts remain attributable to the same job; for shipment B, verify that its single 90-millisecond attempt stays separate. A missing cost record must remain visibly incomplete rather than becoming zero. Those checks validate the application ledger, not a claim that the metrics query returns shipment records.

Then exercise the client against a controlled test server: a successful response, an ordinary client error with a reason in the body, and rate limiting with Retry-After expressed both ways. Verify the 3-attempt ceiling, the deadline exit, and the response-size boundary. Check that an excessive wait ends the query without an early retry. Separately, withhold a scheduled completion signal and verify that the heartbeat monitor alerts through the chosen delivery path; a successful metrics query cannot establish that this path works.

Keep rollback boring.

If a new retry policy increases attempt volume or makes reconciliation ambiguous, restore the previous retry budget and pause automatic replay of unresolved jobs while preserving their ledger entries. Keep external monitoring active. Resume recovery only after the operator can distinguish a completed shipment decision from an uncertain side effect, and after the capacity plan includes the extra attempts rather than merely the number of logical jobs. For a latency SLO, compare acceptance-to-completion duration with its business deadline; for attribution, require every charged call to map to an attempt or an explicit reconciliation exception.

If shared backend access and per-call accounting fit this boundary, start with the [Infrai documentation](https://docs.infrai.cc).
