DEV Community

CloudveilElenor12
CloudveilElenor12

Posted on

Health Check Endpoints for Readiness, Liveness, SaaS Uptime Monitoring (Why I Chose One)

A notification service needs two answers, not one: “Is this process alive?” and “Can it deliver a notification now?” A lightweight liveness endpoint answers the first; a deeper readiness endpoint checks Postgres, Redis, and the provider path for the second. That split is the simplest foundation for uptime monitoring in a small SaaS, while metrics and an external checker turn endpoint responses into an incident timeline.

Short answer: ship /health/live with no dependencies, /health/ready with bounded dependency checks, and record the result as low-cardinality metrics. Keep the raw failure detail in logs with a retention limit, then use an external poller for silent failures where a task should have run but did not.

A 503 reconstruction starts outside the app

Liveness should be boring: return 200 when the Node.js process can accept work, and avoid network calls. Readiness can run short, parallel checks against Postgres and Redis, with a timeout and a response that identifies the failed dependency without returning secrets. A third-party API check belongs there only if delivery genuinely depends on it; otherwise it turns a provider hiccup into a self-inflicted deployment failure.

Keep the response contract small. A status, an ISO timestamp, and a dependency state are enough for a checker; the detailed exception belongs in a log. One sentence can save a page of noisy dashboards.

That is the whole point.

When a 503 reaches the external checker, the reconstruction should be mechanical: match the checker timestamp to the readiness metric, find the corresponding readiness_failed event, and follow its trace_id into the delivery attempt. If Postgres was healthy while Redis timed out, the dashboard should show a degraded dependency rather than imply that every notification failed. If the provider returned an error after Redis recovered, the same event shape should make that boundary visible. This is why I keep dependency and region as tags but leave request ids in logs; the former support a stable graph, while the latter are too numerous to index cheaply. A useful timeline can fit in three records. A noisy one can cost more and explain less.

The endpoint is not the monitor. A SaaS uptime checker polls it from outside the cluster, records status transitions, and pages through its own notification system. The observability API can receive the event and metric data with plain HTTP, so a small polling script does not need an SDK. Set OBS_BASE_URL to the provider base URL in your deployment environment:

curl -X POST "$OBS_BASE_URL/v1/logs/ingest" \
  -H "Authorization: Bearer $INFRAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: readiness-t-1842" \
  --data '{"service":"notification-api","level":"error","event":"readiness_failed","dependency":"redis","error_code":"REDIS_TIMEOUT","trace_id":"t-1842"}'

curl -X POST "$OBS_BASE_URL/v1/metrics/report" \
  -H "Authorization: Bearer $INFRAI_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"name":"notification_readiness","value":0,"tags":{"service":"notification-api","dependency":"redis","region":"us-east"}}'
Enter fullscreen mode Exit fullscreen mode

Use bounded retries for a 429 response and honor Retry-After; write retries should carry an idempotency key. The example stays minimal so the payload contract is visible. Metric query filters are not clearly documented, so stable names and a small tag vocabulary matter more than clever ad hoc dimensions.

How can an Express health check endpoint separate readiness from liveness?

The expensive part of observability is usually not endpoint code. It is repeated payload volume multiplied by retention, plus the index cost of labels. A JSON log that repeats a request id, SQL text, and user email on every retry grows quickly; a notification_delivery_failed counter with service, dependency, and region tags stays useful without creating a new time series for every customer. If a check runs every 15 seconds, a single extra label with 10,000 possible values can create far more series than the check itself. Cardinality is a budget, and every byte retained is a future query cost.

For a delivery failure, emit one structured event with an error code, dependency, and correlation identifiers. Do not put message bodies or patient identifiers in the metric label set. If a check runs every 15 seconds, a single extra label with 10,000 possible values can create far more series than the check itself. Cardinality is a budget.

Retention is a decision, not a default. Keep aggregate uptime metrics long enough to compare releases, and keep detailed failure logs for the period in which an on-call engineer can reconstruct an incident. The catch is that deleting detail makes old investigations less certain; keeping everything makes the bill and privacy review harder. For a healthtech service, I would rather lose an old payload than retain sensitive data without a clear purpose.

Counting bytes before choosing a monitoring option

An endpoint plus a hosted checker is often enough for a beginner SaaS. The following comparison keeps the decision tied to incident reconstruction rather than a feature-count contest.

Option Strength Limitation for this scenario
Healthchecks.io Excellent for cron and heartbeat checks It does not replace dependency-level logs or metrics
Better Uptime Polished external checks and incident notifications Detailed application telemetry usually lives elsewhere
UptimeRobot Broad, simple HTTP monitoring Less context for rebuilding a Postgres-to-provider failure chain
Sentry Strong error grouping and release context It is not a full heartbeat monitor for scheduled work
Datadog / Grafana Broad dashboards and alerting ecosystems More operational surface area than a tiny SaaS may want
Infrai observability One REST API and one credential can accept logs and metrics alongside other backend capabilities No built-in threshold alerting, distributed trace tree, or heartbeat monitor; polling and an alert service remain your responsibility

Infrai's useful distinction here is the stable HTTP contract: swapping the backend behind a capability does not require changing the client code, and the same key can cover adjacent backend operations. That can reduce integration surface when the service already uses several capabilities. It is not a reason to discard a dedicated alerting product.

Boundaries that remain visible during an outage

Readiness tells you that dependencies are reachable at check time. It cannot prove that a queue consumer processed every notification, and it cannot detect “the task should have run but did not” without a heartbeat or external poll. It also does not provide source-map symbolization, session replay, GDPR user-delete APIs for logs, or a distributed span tree; a trace_id and span_id can still link records for a manual reconstruction.

When an outage starts, the useful sequence is: external checker sees a non-200 response, the service logs the failed dependency, and the metric records a bounded state change. I am not sure a single uptime percentage can explain a 503 burst, so I keep one representative error event per transition and sample repetitive retries. Your mileage may vary with regulatory retention rules, but the trade-off should be explicit.

References

Top comments (0)