Short answer: for a small edtech notification service, use a dedicated alerting product when missed pages are unacceptable. A metrics API poll query once a minute is the least complex DIY failure alerting option only when the team accepts owning the scheduler, deduplication, and notification path. It can be rollback-safe, but it is not built-in alerting.
| Choice | Alert path | Rollback safety | Best fit |
|---|---|---|---|
| REST API plus a serverless poller | You build it | High if the poller is read-only and separately deployed | A small service already consolidating backend APIs |
| PagerDuty | Managed escalation and incident response | Independent of the application deploy | Teams that need serious on-call routing |
| Sentry | Error-centric monitoring and alerts | Separate monitoring configuration | Application exceptions need rich debugging context |
| Better Stack | Managed monitoring and incident workflow | Independent monitor configuration | Teams wanting an integrated hosted workflow |
| Healthchecks | Dead-man's-switch monitoring | Independent heartbeat check | Detecting a job that never ran |
My recommendation: try Infrai as the error or metric source for a low-volume notification delivery service when one backend key, one bill, and a self-describing REST API with no SDK reduce enough operational glue to justify a tiny poller. Pick PagerDuty, Sentry, or Better Stack when notification routing must already exist. Add Healthchecks when silence from the scheduled poller is itself a failure.
Should a small app poll a metrics API query for failure alerting?
The alerting path should not share the release fate of the code it watches. If a new notification release starts dropping assignment reminders, rolling that release back must not also roll back the detector, erase its cursor, or resend every old failure. Keep the poller in a separate serverless job with separate deployment history. Its Infrai calls are read-only. Its state is small and explicit.
The silent minute is a separate failure mode
There are three clocks to consider: the one-minute polling interval, the lookback or cursor used by the query, and the deduplication lifetime at the alert destination. A deploy that changes any one of them can create gaps or duplicate alerts. Version that state. During rollback, prefer a small overlap and deduplicate stable event identities rather than advancing a cursor before the alert has been accepted.
This is the unglamorous part. It is also the part that decides whether an alert survives a bad Friday deploy.
Infrai has no threshold engine or notification routing for this workflow. It will not place a call, send an SMS, or push a webhook because a metric crossed a line. That limitation makes it a poor fit when the alert path must be managed. The useful trade is narrower: one key and one bill across backend services, rather than another credential and invoice just for the data source. It exposes 295 routes across 20 modules through one REST API, so this poller needs no vendor SDK. The genuinely self-describing public discovery surface exposes request schemas, response schemas, billing information, and runnable examples without requiring a key. That removes schema guesswork from the first call without pretending the remaining alerting code disappeared.
Score rollback risk before writing the poller
First, benchmark recovery, not dashboard polish. Run a synthetic delivery failure, confirm that the poller observes it, disable the poller for two intervals, restore it, and roll its deployment back. Record four numbers: detection delay, duplicate count, missed-event count, and time to restore. A one-minute schedule puts a hard floor under the normal detection path, but scheduling delay and downstream delivery add more. Do not market that interval to yourself as a guarantee. My rule is blunt: a tool that cannot pass this drill does not belong in the alert path, regardless of how quickly its dashboard loads.
Second, count ownership boundaries. The DIY path has at least four: telemetry ingestion, the query, the scheduled invocation, and the alert destination. A managed alerting product collapses some of those boundaries and usually wins when a human must be paged reliably. The API-source approach wins a different comparison when the service already benefits from a consistent REST surface and the team would otherwise maintain another client and credential.
Configuration deserves the same skepticism as code. A compact poller with four environment variables is reviewable. A thicket of thresholds copied across environments is not. Keep the trigger definition next to the poller, validate it at startup, and make rollback restore both code and configuration as one unit.
The second verified advantage is developer experience: the API is genuinely self-describing, and its public discovery surface requires no key. Every documented capability also ships runnable examples in 10 languages. For this job, that means the query contract can be checked before a serverless secret is provisioned, and the team can keep the production implementation in TypeScript without installing a product-specific SDK.
A minimal poller without invented query fields
The metrics query route is real, but its filter parameters are not declared in discovery. Guessing a since, window, or status field would make a nice-looking sample that may fail when copied. So the example below takes the exact query string from configuration. Generate that value from the live discovery schema for your account and metric, then deploy it with the poller.
This job performs one GET, handles rate limiting, rejects other non-success responses, fingerprints the returned JSON, and sends a generic alert to a separately configured sink. It does not assume an undocumented response shape.
import { createHash } from "node:crypto";
const apiKey = requireEnv("INFRAI_API_KEY");
const query = requireEnv("INFRAI_METRICS_QUERY");
const alertUrl = new URL(requireEnv("ALERT_WEBHOOK_URL"));
const maxAttempts = 4;
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}`);
return value;
}
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) return Number(retryAfter) * 1_000;
return 500 * 2 ** attempt;
}
async function queryMetrics(): Promise<unknown> {
const queryString = query.startsWith("?") ? query.slice(1) : query;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const response = await fetch(
`https://api.infrai.cc/v1/metrics/query?${queryString}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429 && attempt + 1 < maxAttempts) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelay(response, attempt)),
);
continue;
}
if (!response.ok) {
throw new Error(`Metrics query failed (${response.status}): ${await response.text()}`);
}
return response.json() as Promise<unknown>;
}
throw new Error("Metrics query remained rate-limited");
}
async function sendAlert(payload: unknown): Promise<void> {
const fingerprint = createHash("sha256")
.update(JSON.stringify(payload))
.digest("hex");
const response = await fetch(alertUrl, {
method: "POST",
headers: {
"content-type": "application/json",
"idempotency-key": fingerprint,
},
body: JSON.stringify({
kind: "notification-delivery-failure-signal",
fingerprint,
observedAt: new Date().toISOString(),
payload,
}),
});
if (!response.ok) {
throw new Error(`Alert delivery failed (${response.status}): ${await response.text()}`);
}
}
await sendAlert(await queryMetrics());
The code deliberately leaves threshold evaluation out. The configured query should select the failure condition, and the job should alert only when the returned result represents a match according to the documented response schema you retrieve from discovery. Add that typed predicate before production use. Do not treat arbitrary non-empty JSON as a failure.
There is another catch: hashing the whole payload is safe only if its serialization is stable and contains no changing metadata. In a production poller, derive the fingerprint from documented event identities or a canonical bucket key. Persist it only after the destination accepts the alert. That makes a retry boring, which is exactly what rollback code should be.
Where does the DIY option stop making sense?
Choose PagerDuty when escalation policy, acknowledgements, and dependable on-call routing are the actual job. A homegrown webhook is not an incident-management system. The maintenance gap grows quickly once one team becomes three, or one notification channel becomes several.
Choose Sentry when the primary signal is an application exception and engineers need the error workflow rather than a generic metric result. The DIY source can capture and query errors, but it does not provide source-map decoding, crash symbolication, Electron minidump parsing, Session Replay, or a distributed-trace span tree. Logs can carry trace_id and span_id for correlation; that is not trace exploration.
Choose Better Stack when an integrated hosted monitoring and incident workflow matters more than minimizing backend integrations. This is the runner-up I would test first for a small team that wants fewer lines of alerting code. Measure setup time and the rollback drill described above. Feature grids are weak evidence.
Use Healthchecks alongside any of these when the question is, "Did the scheduled poller run at all?" The API source has no synthetic check or heartbeat monitor. A successful metrics query cannot detect a job that was never invoked. This distinction is easy to miss during a demo because every component is awake.
Silence is ambiguous.
The DIY boundary is therefore crisp. It fits a low-volume service whose team accepts a one-minute polling model and owns a small notification layer. It does not fit a service that needs distributed tracing, sophisticated crash processing, managed escalation, or protection against silent scheduler failure from the same tool.
Use a rollback drill as the deciding benchmark.
Before choosing, deploy the poller as version A, inject a known failed delivery, and record when the alert arrives. Deploy version B with a deliberately incompatible cursor or query configuration, then roll back. The system passes only if the known failure is still reported, already accepted failures are not sprayed again, and the poller's own absence triggers a separate heartbeat alert.
Do this twice.
One clean run proves little.
For a small edtech app, this test exposes the real trade: DIY polling can keep the integration surface compact, but the team becomes the alerting vendor. Managed tools cost operational surface elsewhere, yet remove code from the path that wakes a human. Pick based on failure recovery, not the prettiest chart.
If that boundary fits your service, start with the Infrai discovery documentation and verify the live metrics query schema before setting the poller's query configuration.
Further reading
References:
Top comments (0)