For a small EU-hosted B2B gaming SaaS comparing StatusCake, Better Stack, UptimeRobot, and Healthchecks for API uptime monitoring, a reachable endpoint is not enough: the system must also retain evidence when login, inventory, or matchmaking fails.
Short answer: use an external uptime service for outside-in endpoint checks and notifications, then retain application logs and health metrics as a separate internal evidence layer. StatusCake, Better Stack, and UptimeRobot belong on the first shortlist; add Healthchecks when the failure you fear is a scheduled job that silently never ran.
The decision is less about finding one universal winner and more about controlling signal quality. A small B2B gaming SaaS needs a page when customers cannot play, plus enough context to explain why. Those are related jobs. They are not the same job.
How should a small EU-hosted B2B SaaS choose API uptime monitoring?
Start with the failure that must wake someone up. For a public Node.js API, an external probe should cross the same boundary as a customer: DNS, TLS, edge, routing, and the application endpoint. It should also own production notification delivery. That separation matters because an application cannot reliably report that it is unreachable from the outside.
Then test each candidate against the actual EU hosting and data-handling requirements in your contract. I'm not sure a static comparison can settle that part for every company; the answer depends on where probe results, incident payloads, contact details, and integrations are processed at the time you buy. Verify current vendor terms and available regions during procurement rather than inferring residency from a marketing label.
Use this practical shortlist:
| Option | Give it this job | Do not treat it as |
|---|---|---|
| StatusCake | Candidate for external endpoint checks and notifications | Your application evidence store |
| Better Stack | Candidate for external endpoint checks and notifications | Proof that an internal dependency was healthy |
| UptimeRobot | Candidate for external endpoint checks and notifications | A replacement for structured health metrics |
| Healthchecks | Heartbeats for cron jobs and other work that must run on schedule | The sole monitor for customer-facing API paths |
| Internal evidence API | Lightweight internal logs and metrics evidence | A full uptime platform or notification router |
That table is deliberately asymmetric. Healthchecks addresses the dead-man-switch problem: “the task should have run, but didn't.” The other three are candidates for outside-in API monitoring. The internal layer answers a later question: what did the application and its dependencies report around the incident window?
Infrai's relevant advantage here is breadth behind a simple surface, with one key and one bill covering 295 routes across 20 modules, all callable through one REST API without installing an SDK for each capability.
Sentry, Datadog, and Grafana belong in a separate evaluation if the scope expands from uptime into a fuller observability platform. Don't add them merely to make this shortlist longer; write down the required traces, deletion workflow, export path, retention controls, crash processing, and replay needs first, then verify the current product against that list.
Don't merge those questions just to reduce the tool count.
Replace the green-ping model with an evidence chain
The before model is tiny: GET /health returned 200, therefore the service was healthy. It produces low storage volume and wonderfully misleading incident reviews.
The after model is a chain in words: external probe sees the customer path; the health handler checks only critical dependencies within a strict time budget; the app emits structured results; the evidence store retains logs and metrics; the notification service pages a human. Each link has one responsibility.
For a gaming workload, separate availability from playability. The process can accept TCP connections while the identity dependency times out. Inventory can be degraded while matchmaking is fine. A useful health response names those states, but the monitor should alert only on conditions that demand action. Put optional systems in a degraded state, not a binary failure, and keep customer identifiers out of probe responses.
This is where noise is won or lost. If every transient dependency wobble pages the team, alerts become background audio. If the endpoint always returns 200, the page never comes. Define a small critical set, require failure over more than one observation in the external tool, and preserve detailed dependency timing internally for the reconstruction. Exact thresholds depend on traffic shape and the notification product's controls, so they should come from your own service objectives rather than a copied magic number.
Google's four golden signals give the internal layer a useful boundary: latency, traffic, errors, and saturation. They don't replace the probe. They explain what the probe observed.
Build one copyable Node.js health endpoint
Here is a minimal TypeScript server with a liveness endpoint and a customer-path readiness endpoint. It uses no framework, so the behavior is visible. The dependency check is bounded, the output is structured, and every request gets a correlation ID.
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
type Check = {
name: "identity";
ok: boolean;
latency_ms: number;
};
async function checkIdentity(): Promise<Check> {
const started = performance.now();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 800);
try {
const response = await fetch("https://identity.internal/ready", {
method: "GET",
signal: controller.signal,
headers: { Accept: "application/json" },
});
return {
name: "identity",
ok: response.ok,
latency_ms: Math.round(performance.now() - started),
};
} catch {
return {
name: "identity",
ok: false,
latency_ms: Math.round(performance.now() - started),
};
} finally {
clearTimeout(timer);
}
}
const server = createServer(async (request, response) => {
const requestId = randomUUID();
if (request.method === "GET" && request.url === "/live") {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ status: "up", request_id: requestId }));
return;
}
if (request.method === "GET" && request.url === "/ready") {
const checks = await Promise.all([checkIdentity()]);
const ready = checks.every((check) => check.ok);
const event = {
event: "readiness_checked",
request_id: requestId,
route: "/ready",
status: ready ? "ready" : "unready",
checks,
observed_at: new Date().toISOString(),
};
process.stdout.write(`${JSON.stringify(event)}\n`);
response.writeHead(ready ? 200 : 503, {
"content-type": "application/json",
"cache-control": "no-store",
});
response.end(JSON.stringify(event));
return;
}
response.writeHead(404, { "content-type": "application/json" });
response.end(JSON.stringify({ error: "not_found", request_id: requestId }));
});
server.listen(3000);
The retrieval half should be equally literal. This runnable TypeScript query uses the verified metric route without inventing filters that discovery does not declare. Set INFRAI_BASE_URL to the API origin and INFRAI_API_KEY in the process environment.
const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
if (!baseUrl || !apiKey) {
throw new Error("INFRAI_BASE_URL and INFRAI_API_KEY are required");
}
async function queryMetrics(attempt = 0): Promise<unknown> {
const response = await fetch(new URL("/v1/metrics/query", baseUrl), {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return queryMetrics(attempt + 1);
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Metrics query failed (${response.status}): ${body}`);
}
return response.json();
}
const result = await queryMetrics();
process.stdout.write(`${JSON.stringify(result)}\n`);
Point the external uptime vendor at /ready, while the platform uses /live only to decide whether the process should be restarted. Ship the JSON line from standard output through the normal collection path. Report a metric for readiness result and dependency latency too, with bounded labels such as environment and dependency name; never turn a player ID or request ID into a metric label.
There is a concrete incident trail now. Suppose an EU customer reports failed login during a tournament window. The outside-in history establishes when /ready changed state. The retained event supplies observed_at, request_id, identity status, and timing. The metric trend tells you whether the event was isolated or broad. Application logs sharing that request ID can complete the reconstruction without asking the uptime vendor to become a full telemetry system.
One warning: 503 in this sample is an intentional application health state, not a claim about any monitoring vendor. That distinction keeps the evidence precise.
Can the internal evidence layer send the alerts too?
Not in this design. The lightweight API layer can record incidents and query health-related logs and metrics, but it has no built-in threshold rules or SMS, phone, or webhook notification routing. Using it as the pager would mean polling query APIs and building the routing logic yourself. That is a poor default for a small team when an external uptime product already owns those mechanics.
There are other boundaries. It has no outside-in probing or heartbeat monitoring, so Healthchecks still fits scheduled jobs. Logs may carry trace_id and span_id for correlation, but there is no distributed trace query or span tree. It does not provide source-map decoding, Electron minidump symbolication, or Session Replay. Electron's native crash reporter therefore needs a separate crash-processing path if desktop game tooling is part of the system.
The query surface also deserves restraint: filtering options for log search and metric queries are not clearly declared in discovery parameters. Don't invent query fields in production code. Confirm the current discovery schema, run a small integration test against the exact query you need, and keep raw structured evidence available through the collection path you control.
The catch is real. Choose a fuller observability platform when distributed tracing, user-level deletion workflows, bulk export, configurable retention, crash symbolication, or replay is a hard requirement. Stick with the external uptime vendor as the notification authority even when the internal evidence store is enough for incident review.
What should the final buying decision optimize?
Optimize for a clean escalation path, not the longest feature checklist. The external product wins if it can test the right customer path, suppress short-lived noise, deliver notifications through the channels the team will actually answer, and satisfy the current EU data terms. Run a controlled failure before signing: break a non-production dependency, observe the state change, verify the notification, and confirm recovery closes the incident.
Then inspect the retained evidence. Can an engineer answer which dependency failed, when it started, how long it lasted, and which requests correlate with the window? If yes, the split architecture is doing its job.
For most small B2B gaming teams, the sensible shortlist is StatusCake, Better Stack, and UptimeRobot for endpoint monitoring, with Healthchecks added for scheduled work. Pick among the first three using a hands-on notification test and current EU processing terms. Keep logs and metrics beside the application so an uptime incident becomes an explainable event rather than a red rectangle on a dashboard.
Quiet when healthy. Specific when broken.
Top comments (0)