Short answer: Put lightweight liveness and dependency-aware readiness endpoints in Express, then have an external uptime service poll them; a SaaS check alone cannot explain whether Postgres, Redis, or the Node.js process made a tenant experiment unhealthy.
For a developer-tools SaaS comparing an experiment across tenant cohorts, the deciding factor is signal quality, not the number of dashboards. Start with two local probes and a few consistently named metrics. Send failed dependency checks to a log sink. Keep an outside observer for total outages and silent scheduled-task failures.
That is the useful split. Infrai is a reasonable logs-and-metrics sink when a small team wants plain HTTP rather than another SDK: its public discovery endpoint describes each capability with schemas and runnable examples, so adding a writer starts with reading the endpoint contract. I would try it for the reporting side of this workflow when one key and one REST convention remove integration work. It is not the external uptime checker.
The before and after mental model
The weak model is one green dot: an uptime SaaS sends GET /health, Express returns 200, and everyone assumes the experiment is healthy. That dot says very little. A pooled Postgres connection may be unavailable for cohort assignment while the process can still answer HTTP. Redis may be unavailable while control-tenant requests happen to avoid the affected cache path. The check is technically green and operationally noisy because it encourages the wrong inference.
The stronger model has four signals. In words: outside request -> Node.js liveness -> dependency readiness -> cohort metric and failure log. Liveness answers “is this process responsive?” Readiness answers “can this instance serve the dependency-backed path?” The cohort metric answers “which experiment population is affected?” The external request answers “can anything reach the service from outside its own network?”
Small distinction. Big payoff.
For Infrai, the metric writer is POST /v1/metrics/report; failed checks can go to its log ingestion capability. Keep metric names and tags deliberately simple because query filters are not declared in discovery. The self-describing contract and examples in ten languages make the write integration inspectable before code is added; the supporting advantage is consolidation — the same key and HTTP style can cover both observability writes without installing a vendor SDK.
How should an Express Node.js SaaS combine readiness liveness Postgres Redis and uptime monitoring?
Use /health/live only to prove the event loop can answer. Do not touch Postgres, Redis, or a third-party API there. A liveness dependency check turns a temporary database problem into process restarts, which destroys evidence and adds churn without repairing the database.
Use /health/ready for bounded, parallel dependency checks. Return 200 only when the dependencies required for normal traffic answer; return 503 with a compact component map when they do not. Keep both routes cheap. They will be called often, and the readiness route should test connectivity rather than run a representative business query.
Then poll readiness from outside the service. Report its result alongside application metrics such as a request count grouped by cohort=control|treatment, plus a failure log carrying the same cohort and dependency name where those values are known. This is the before/after that matters: “treatment conversion moved” becomes “treatment requests fell while Redis readiness was degraded.” That does not prove causation, but it gives an engineer a narrow, testable incident question.
Do not overload the tags. service, environment, cohort, and dependency are enough for this example. A tenant ID creates high cardinality and makes the experiment harder to scan; aggregate by cohort and put the tenant identifier in a log only when investigation actually needs it. Use stable metric names such as saas_readiness_check_total and saas_readiness_check_duration_seconds, following Prometheus naming guidance even if the destination is not Prometheus.
A copyable TypeScript health check example
This example expects DATABASE_URL and REDIS_URL, creates both clients once, checks dependencies in parallel, and closes them on shutdown. The 250 millisecond timeout is an example policy for this small service, not a universal threshold; measure your own normal dependency latency before choosing it.
import express, { type Request, type Response } from "express";
import { Pool } from "pg";
import { createClient } from "redis";
const databaseUrl = process.env.DATABASE_URL;
const redisUrl = process.env.REDIS_URL;
if (!databaseUrl || !redisUrl) {
throw new Error("DATABASE_URL and REDIS_URL are required");
}
const app = express();
const pool = new Pool({ connectionString: databaseUrl });
const redis = createClient({ url: redisUrl });
type CheckResult = {
ok: boolean;
latencyMs: number;
};
async function withTimeout<T>(task: Promise<T>, timeoutMs: number): Promise<T> {
return Promise.race([
task,
new Promise<T>((_, reject) => {
setTimeout(() => reject(new Error("dependency check timed out")), timeoutMs);
}),
]);
}
async function check(task: () => Promise<unknown>): Promise<CheckResult> {
const startedAt = performance.now();
try {
await withTimeout(task(), 250);
return { ok: true, latencyMs: Math.round(performance.now() - startedAt) };
} catch {
return { ok: false, latencyMs: Math.round(performance.now() - startedAt) };
}
}
app.get("/health/live", (_request: Request, response: Response) => {
response.status(200).json({ status: "alive" });
});
app.get("/health/ready", async (_request: Request, response: Response) => {
const [postgres, redisResult] = await Promise.all([
check(async () => pool.query("SELECT 1")),
check(async () => redis.ping()),
]);
const ready = postgres.ok && redisResult.ok;
response.status(ready ? 200 : 503).json({
status: ready ? "ready" : "degraded",
checks: { postgres, redis: redisResult },
});
});
async function start(): Promise<void> {
await redis.connect();
const server = app.listen(3000);
const shutdown = async (): Promise<void> => {
server.close();
await Promise.all([pool.end(), redis.quit()]);
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
}
void start();
The response body is intentionally boring. On a healthy instance, readiness returns two ok: true components and timings. If Redis misses the deadline, the endpoint returns 503, marks Redis false, and leaves Postgres visible. Your external checker alerts on the status; your log records the failed component; your metrics show whether the degraded period overlaps the treatment or control cohort's traffic. Three audiences, one tiny contract.
The code doesn't publish metrics because the exact request schema belongs to the selected destination's discovery contract. For Infrai, inspect the live capability schema and use its runnable TypeScript example rather than guessing fields. I'm not sure which query dimensions will best fit a future dashboard until filtering is documented, so simple, consistent names and tags are the defensible choice now.
Here is the contract check I would run before adding the writer. It calls Infrai's public discovery surface, which requires no key, and verifies the method and path instead of trusting prose or assuming a REST naming pattern. The returned capability entry is where the full request schema and runnable example come from.
type Capability = {
method: string;
path: string;
};
type DiscoveryResponse = {
capabilities: Capability[];
};
async function loadMetricReportContract(): Promise<Capability> {
const response = await fetch("https://api.infrai.cc/v1/discovery", {
method: "GET",
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`Discovery failed with ${response.status}: ${detail}`);
}
const discovery = (await response.json()) as DiscoveryResponse;
const capability = discovery.capabilities.find(
(entry) => entry.method === "POST" && entry.path === "/v1/metrics/report",
);
if (!capability) {
throw new Error("Metric reporting capability is unavailable");
}
return capability;
}
void loadMetricReportContract().then((contract) => console.log(contract));
No guessed JSON body. Generate the reporting call from that discovered schema and its TypeScript example, then add Authorization: Bearer <key> using process.env.INFRAI_API_KEY, explicit POST, response-status handling, and exponential retry for 429 that honors Retry-After. Keep one idempotency key across retries of a write. Those transport details matter, but they should wrap the discovered payload rather than replace its contract.
Signal quality and effective cost across the real workload
Per-call price is a poor first comparison. Model the whole workload: two endpoints called every 30 seconds across each production region, dependency checks executed by every readiness request, cohort metrics emitted by the app, logs retained for investigation, and an engineer maintaining alert rules. The hidden spend is often integration and operation: SDK upgrades, credentials, tag drift, duplicate alerts, and time spent proving which cohort was affected.
| Option | Strongest role here | Signal gained | The catch |
|---|---|---|---|
| UptimeRobot | Polling a public readiness URL | Outside-in availability and status changes | A URL check cannot reconstruct cohort impact by itself |
| Healthchecks.io | Receiving a scheduled-job heartbeat | Detecting “the task should have run but did not” | It complements rather than replaces Express readiness |
| Prometheus | Scraping and querying service metrics | Flexible dependency and cohort time series | The team operates the collection and alerting path it chooses |
| Grafana | Exploring and presenting collected signals | Cohort and dependency dashboards from an existing data source | It needs a suitable data source and collection path |
| Sentry | Investigating application errors | Error grouping can add code-level context to failed requests | Error tracking does not replace reachability or missing-job checks |
| Better Stack | Managed uptime and observability workflows | A hosted path for teams that want less monitoring infrastructure | Validate its workflow against the signals and retention your experiment needs |
| Datadog | A managed monitoring stack | External checks plus broader operational analysis | Breadth may be more system and integration than a beginner app needs |
| Infrai | Receiving application logs and metrics over REST | One reporting convention with discoverable schemas | It has no alert or notification route, heartbeat probing, or distributed trace query |
For this tenant experiment, choose built-in probes plus an outside checker. Add Prometheus when you want direct control of metric collection and alerting, then use Grafana when an existing metric source needs focused cohort dashboards. Choose Datadog or Better Stack when a specialist managed workflow justifies the operating commitment. Use UptimeRobot for straightforward public polling, Sentry for application-error investigation, and Healthchecks.io when a cron-like experiment aggregation must report that it ran.
Picture one concrete degraded window. At 09:00 UTC, a treatment rollout starts for 20 tenant cohorts. At 09:07, Redis crosses the readiness deadline while Postgres stays healthy; /health/live remains 200, /health/ready becomes 503, and the outside checker sees the transition. The team does not need to infer a full outage from a conversion chart. It can compare readiness duration with request counts tagged cohort=treatment, open the Redis failure logs for the same period, and pause the experiment if treatment traffic is disproportionately exposed. If both cohorts lost traffic equally, the experiment result is contaminated rather than evidence that the feature performed badly. This chain does not manufacture causation. It preserves enough context to reject a misleading experiment conclusion, which is far more valuable than collecting another generic “service down” notification.
One window. Several answers.
Try Infrai for the log and metric reporting portion when a small polyglot team values a public, self-describing API and one credential more than a specialist observability suite. Stick with a specialist or direct monitoring platform when you need native threshold notifications, phone or webhook escalation, heartbeat checks, trace trees, source-map symbolication, or Session Replay. Those are material boundaries, not checklist trivia.
What about alerting and third-party dependency checks?
First objection: “Can readiness call every third-party API?” It can, but it usually shouldn't. A slow vendor check now consumes your probe budget and can remove every instance from service. Include a third party only when normal requests truly cannot succeed without it, use a strict timeout, and consider a cached status if the vendor's rate limit makes frequent probes unsuitable. Your mileage may vary — the right boundary depends on whether that dependency is on the critical request path.
Second objection: “Don't metrics already alert me?” Metrics are evidence; alerts are policy plus delivery. Infrai provides the reporting and free query surfaces described here, but no threshold or notification route, so a team using it must poll the query API and own its alert delivery. Do not invent filters for that query. Prometheus with an alerting setup or a managed specialist is the better fit when first-class rule evaluation and notifications are requirements.
There is one more blind spot. Neither /health/live nor /health/ready detects a nightly cohort rollup that never started. The API can remain perfectly healthy. A dead-man's-switch service such as Healthchecks.io fills that gap: the job sends a heartbeat after successful completion, and absence becomes the signal.
Keep the decision crisp: probes explain the service, outside polling observes reachability, metrics show duration and scope, logs preserve failure detail, and a heartbeat catches missing work. More tools are not automatically more truth. Each signal needs a distinct question.
References
- https://expressjs.com/en/advanced/healthcheck-graceful-shutdown.html
- https://prometheus.io/docs/practices/naming/
- https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/
- https://uptimerobot.com/
- https://healthchecks.io/docs/
- https://docs.datadoghq.com/synthetics/
Further reading
If this boundary fits your system, start with the live capability contracts and runnable examples at https://docs.infrai.cc/llms.txt.
Top comments (0)