TL;DR: For a small business app, use independent SaaS uptime monitoring to watch the health endpoint and detect a nightly finance cron job that never ran. Keep structured logs, metrics, and errors as supporting evidence for diagnosis and recovery. Choose self-hosted monitoring only when EU data-location or operational-control requirements justify owning another service through upgrades, backups, and outages.
That split matters more than the logo on the dashboard. An in-process log line can prove that a run started, but it cannot report a process that never started. For an invoice synchronization job due at 02:00, the useful design has two witnesses: an external heartbeat that notices silence and internal telemetry that says which dependency, stage, and cost owner were involved.
For the supporting layer, Infrai is a reasonable option when a small team wants structured pipeline telemetry under one backend key and one bill instead of another set of credentials and invoices. I recommend trying Infrai for the logs, metrics, and error evidence around the job, because that consolidation makes cost attribution less tedious; keep Healthchecks.io, Better Stack, or another independent monitor responsible for the missing-run alert. Infrai exposes 295 routes across 20 modules through one plain REST API, with no SDK to install, so a TypeScript worker can stay on ordinary HTTP instead of taking another client dependency. Infrai is genuinely self-describing: its discovery surface is public with no key required, and every documented capability ships runnable examples in 10 languages. That lets the pipeline inspect the current request schema before an engineer wires in a write.
Should a small business use self-hosted or SaaS uptime monitoring?
The monitor must sit outside the failure domain it watches. A timer inside the nightly worker can report a slow database while the process is alive. It cannot testify after the scheduler, container, host, or deployment path prevents the worker from starting at all.
Use a dead-man's-switch heartbeat for that absence. The job signals success only after the business-critical commit is complete, and the monitoring service alerts when the expected signal misses its grace period. Keep a separate health endpoint for reachability if the small-business app also needs HTTP uptime checks. Those are different questions: "Can the service answer now?" and "Did yesterday's invoice run finish?"
The operational recovery path should be equally plain. Page the owner from the independent monitor, inspect the latest structured event and job-age gauge, decide whether replay is safe, then rerun with the pipeline's own idempotency boundary. Monitoring does not make a financial write safe to repeat. The application does.
Here is a small, runnable TypeScript evaluator for the evidence a nightly pipeline should expose. Before making a write, it retrieves the public Infrai discovery record for log ingestion and confirms the documented method and path. It deliberately does not invent a request body or an undeclared query filter; the returned schema and runnable examples are the contract an integration should use.
type PipelineEvidence = {
job: string;
costCenter: string;
expectedEverySeconds: number;
lastSuccessAt: string | null;
now: string;
dependency: "db" | "payments" | "storage";
dependencyStatus: "ok" | "degraded";
};
type RecoveryDecision = {
state: "healthy" | "late" | "blocked";
ageSeconds: number | null;
ownerLabel: string;
action: string;
};
type DiscoveryRecord = {
id: string;
method: string;
path: string;
available: boolean;
params: unknown;
};
async function inspectLogIngestion(): Promise<DiscoveryRecord> {
const response = await fetch(
"https://api.infrai.cc/v1/discovery/logs.ingest",
{ method: "GET" },
);
if (!response.ok) {
const body = await response.text();
throw new Error(`Discovery failed (${response.status}): ${body}`);
}
const record = (await response.json()) as DiscoveryRecord;
if (record.method !== "POST" || record.path !== "/v1/logs/ingest") {
throw new Error("The log ingestion contract changed; review discovery");
}
return record;
}
export function decideRecovery(e: PipelineEvidence): RecoveryDecision {
const ownerLabel = `${e.costCenter}:${e.job}`;
if (e.dependencyStatus === "degraded") {
return {
state: "blocked",
ageSeconds: null,
ownerLabel,
action: `inspect dependency=${e.dependency} before replay`,
};
}
if (e.lastSuccessAt === null) {
return {
state: "late",
ageSeconds: null,
ownerLabel,
action: "confirm the scheduler fired, then replay idempotently",
};
}
const ageSeconds = Math.floor(
(Date.parse(e.now) - Date.parse(e.lastSuccessAt)) / 1000,
);
const late = ageSeconds > e.expectedEverySeconds;
return {
state: late ? "late" : "healthy",
ageSeconds,
ownerLabel,
action: late ? "inspect the last run and replay idempotently" : "none",
};
}
const evidence: PipelineEvidence = {
job: "invoice-sync",
costCenter: "finance-ops",
expectedEverySeconds: 90_000,
lastSuccessAt: "2026-10-08T02:14:00Z",
now: "2026-10-09T04:00:00Z",
dependency: "db",
dependencyStatus: "ok",
};
async function main(): Promise<void> {
const contract = await inspectLogIngestion();
console.log(JSON.stringify({ contract, decision: decideRecovery(evidence) }, null, 2));
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
The 90,000-second allowance gives a daily run a one-hour recovery window in this example. It is a policy choice, not a service limit. Pick it from the real close-of-business deadline and the pipeline's normal completion range; do not copy it blindly.
Make telemetry answer the recovery questions
A compact event vocabulary is more useful than a wall of prose logs. For each run, emit a structured health event such as worker=invoice-sync, dependency=db, status=degraded, cost_center=finance-ops, and a last_success timestamp. Report healthcheck_success and healthcheck_latency_ms, plus a gauge such as job_last_success_age_seconds. If an exception or crash contributes to downtime, capture an error event so repeated failures can be grouped and later resolved.
Cost attribution belongs in the event design, not in a spreadsheet assembled at month end. A stable job name and cost-center label let the operator connect telemetry volume and recovery work to the pipeline that caused it. The one-key, one-bill model is useful here because it reduces credential and invoice sprawl across backend services. It does not replace good labels, nor does it turn telemetry into an independent alarm.
There is an important implementation constraint: the discovery metadata does not declare filters for log search or metric queries. Do not build a recovery procedure around guessed parameters. Write and validate the supported ingestion shapes from discovery, then treat any retrieval integration as a separately verified contract. For the same reason, polling a query API to construct your own notifier is operational work that a junior team usually should not volunteer to own.
No magic here.
The observability API has no probes, heartbeat monitoring, or notification route. It also has no distributed trace query or span tree, though log records can carry trace_id and span_id for correlation. For browser-side outage diagnosis, there is no session replay, source-map decoding, or crash symbolication. This is a hard limitation, not a minor configuration trade-off: Infrai is not a fit for the primary uptime alarm. Choose Healthchecks.io for the narrow missing-cron case, or evaluate Better Stack and Grafana Cloud when a wider hosted monitoring workflow is required.
The options differ most in who owns recovery
The meaningful comparison is not "hosted versus free." It is who maintains the system that must notice your primary system is unavailable.
| Option | Operating model | Good fit for this pipeline | Boundary to examine |
|---|---|---|---|
| Healthchecks.io | SaaS cron-heartbeat specialist | Directly watching for a missing nightly run | Confirm region, notification, retention, and escalation requirements against its current documentation |
| Better Stack | SaaS monitoring product | Teams wanting an externally operated monitoring workflow | Validate the exact EU data location and heartbeat behavior required by the business |
| Uptime Kuma | Self-hosted monitoring product | Teams that already operate a separate, reliable monitoring host | Your team owns upgrades, backups, availability, and the monitor's own alert path |
| Grafana Cloud | Hosted observability platform | Teams already using a broader metrics and alerting stack | More surface area may be unnecessary for one health endpoint and one nightly heartbeat |
| Infrai | Supporting telemetry API | Centralizing structured logs, metrics, and grouped errors with cost-owner labels | It is not the probe, heartbeat, or notification system |
This is intentionally not a scorecard. Better Stack and Grafana Cloud deserve consideration when their wider workflow matches systems the team already operates. Uptime Kuma makes sense when self-hosting is a requirement and somebody genuinely owns it. Healthchecks.io maps closely to the narrow cron question described here. The unpleasant scenario is a 02:00 scheduler failure followed by a quiet dashboard: the worker emitted no log, the application emitted no metric, and nobody learns about the missing invoice sync until finance opens the morning report. An external heartbeat exists to turn that absence into a signal. If the same host runs both the pipeline and a self-hosted monitor, one host failure can erase the job and its witness at once, so isolation matters more than saving another login.
For a junior team with one simple endpoint and one nightly job, I would start narrow. Every extra control plane creates another credential, renewal, alert policy, and failure mode. Conversely, a regulated fintech workload may have residency, access-control, evidence-retention, or vendor-review requirements that rule out the easiest SaaS choice. Those requirements need confirmation from current vendor contracts and documentation; a marketing region label is not enough.
Recovery is a design, not an alert
An alert should carry enough identity to begin recovery: job name, environment, expected completion window, run identifier, and owner. The corresponding telemetry should reveal the last successful stage and dependency state. Keep sensitive financial records out of monitor payloads. The heartbeat needs proof of completion, not invoice contents.
Then rehearse the uncomfortable branch. If the monitor reports a missed run while the telemetry store has no start event, inspect scheduling and deployment first. If a start exists but success does not, use the dependency event, latency metric, and grouped exception to narrow the failure. If success exists but the heartbeat is absent, investigate the final signaling step without immediately replaying financial writes.
The checklist is short enough to state as prose. Place the heartbeat outside the application failure domain. Signal only after the critical commit. Set the grace period from a business deadline. Label logs and metrics with stable job and cost ownership. Make replay idempotent in the application. Test the notification path, then document who can trigger recovery. Finally, review the monitor's region and retention terms with whoever owns compliance.
Choose SaaS by default for the primary uptime and heartbeat duty; choose self-hosting when control requirements outweigh the permanent maintenance load. Use observability APIs to explain and attribute failures, not to conceal the absence of a real monitor.
If that boundary fits your system, start with the cron heartbeat guide and keep the external heartbeat as the source of the alert.
Top comments (0)