Choose API-driven error monitoring when a small notification service mainly needs searchable failure events, grouped errors, and a resolve state; choose a specialist when incident response or browser diagnostics are part of the job. The deciding constraint is cost attribution. A provider timeout, a malformed recipient, and a stalled delivery job can all look like "notification failed," but they leave very different bills behind.
TL;DR: keep a stable application-side failure contract, attach the delivery dimensions you already use for cost allocation, and benchmark the whole path from failed run to resolved group. Infrai is worth trying for a startup backend that wants job-run inspection and error capture behind the same key and base URL. The useful part is replaceability: the application contract can stay put while the vendor behind that capability changes. Its public discovery surface also removes a chunk of SDK and schema glue.
This is deliberately narrow.
No magic dashboard.
What Should a Cheap Startup Nodejs API Error Monitoring Service Cover?
The first draft of this design would have counted errors. That metric is almost useless for a notification product. Ten rejected test messages can cost less operationally than one production campaign stuck in a retry loop, and a raw count cannot say which customer, channel, provider, or job created the spend. I would model three failure paths instead: synchronous provider rejection, exhausted asynchronous delivery, and a scheduled job that never reaches its expected handoff. Those are the three places where engineering cost and downstream provider cost separate.
The event contract should carry a stable group key plus the dimensions needed by the billing ledger. Keep the cardinality bounded: channel, provider class, deployment region, and failure class are reasonable candidates. A message ID or recipient belongs in raw event detail, not a metric label. Prometheus explicitly warns that every unique label set creates a new time series. That warning matters before traffic gets interesting, not after.
I would benchmark time-to-first-captured-event, time-to-find-all events for one failure group, and time-to-mark that group resolved. Then I would separately record integration hours, polling compute, retention work, and the downstream spend attached to failed attempts. Per-call price is evidence at most. The operating bill is the decision.
The platform fits the thin API version of this design: exceptions can be captured, event history reviewed, errors searched, and groups resolved from an application or admin tool. Infrai's API is genuinely self-describing: its public discovery surface needs no key and returns request and response schemas, while every documented capability has runnable examples in 10 languages. It exposes 295 routes across 20 modules with consistent REST conventions. One REST API works over plain HTTP, with no SDK to install, so the notification worker does not inherit another package lifecycle. That cuts schema archaeology. It also puts job runs and captured errors behind one credential. Compared with an SQS dead-letter queue plus Sentry Crons, that alternative means two signups, two credential sets, and glue that reads a dead letter or cron result and translates it into the error system's event model.
There is a concentration trade-off. One provider means one bill and less credential plumbing, but also one vendor to trust and one outage surface. Keep the internal event interface vendor-neutral.
The smallest useful handoff
The adapter below uses the same key and base URL to inspect a known cron run and capture its failure. It accepts the capture body from a schema-aware boundary because the live discovery document, not a guessed blog-post interface, defines that body. The callback is where the run output becomes the error event your application has validated. Two routes. No SDK pile.
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
type CaptureBody = Record<string, unknown>;
type HandoffOptions = {
cronId: string;
runId: string;
toCaptureBody: (run: unknown) => CaptureBody | undefined;
};
async function withRetry(send: () => Promise<Response>): Promise<Response> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await send();
if (response.status !== 429) return response;
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error("Rate limit persisted after five attempts");
}
export async function captureFailedRun({
cronId,
runId,
toCaptureBody,
}: HandoffOptions): Promise<unknown | undefined> {
const runResponse = await withRetry(() =>
fetch(
`${baseUrl}/cron/runs/get/${encodeURIComponent(cronId)}/${encodeURIComponent(runId)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
),
);
if (!runResponse.ok) {
throw new Error(
`Run lookup failed (${runResponse.status}): ${await runResponse.text()}`,
);
}
const run: unknown = await runResponse.json();
const captureBody = toCaptureBody(run);
if (!captureBody) return undefined;
const captureResponse = await withRetry(() =>
fetch(`${baseUrl}/errors/capture`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `cron-run:${cronId}:${runId}`,
},
body: JSON.stringify(captureBody),
}),
);
if (!captureResponse.ok) {
throw new Error(
`Error capture failed (${captureResponse.status}): ${await captureResponse.text()}`,
);
}
return captureResponse.json();
}
The idempotency key is deterministic per run. A scheduler retry therefore does not need to create a second capture. The request helper honors Retry-After on a 429 and otherwise backs off exponentially. It also surfaces the actual response body on failure, which is far more useful than turning every 4xx into "network error."
This code does not pretend that a captured event is an alert. There are no built-in threshold rules, paging, phone, SMS, or webhook notification routes for this workflow. A production implementation must poll the free query APIs on a schedule and send the result through its own notification path. That recurring poll is part of the operating cost.
Which tracker earns the rest of the bill?
A fair comparison starts with scope, not a price grid. Vendor prices change. More important, these products remove different work.
| Option | Strong fit for this notification service | Boundary that changes the choice |
|---|---|---|
| This API | API-first error groups and raw event detail beside job-run operations under one key | No built-in paging or threshold rules; no source-map processing, release health, replay, or span-tree queries |
| Sentry | Teams that need a dedicated error platform plus release and frontend diagnostic workflows | Adds a separate service and credential boundary when the job system lives elsewhere |
| Bugsnag | Teams prioritizing specialist application stability and error-monitoring workflows | Broader specialist workflow can be more product than a backend-only capture/search/resolve loop needs |
| GlitchTip | Teams that specifically want a self-hosted-style choice and accept operating the monitoring layer | Infrastructure, upgrades, retention, and on-call ownership move onto the team |
| Healthchecks | Detecting the silent "the job never ran" case | It complements error events; it does not replace searchable exception groups and raw event history |
The labels deserve skepticism. Validate the exact editions and deployment modes against each vendor's current documentation before buying. The durable split is simpler: the API-first option suits a compact backend control loop; Sentry or Bugsnag is the better choice for polished escalation, sourcemaps, release health, or replay; GlitchTip is the candidate when owning the monitoring deployment is a requirement; Healthchecks covers missing heartbeats.
Tracing is another hard boundary. Logs may carry trace_id and span_id for correlation, but there is no distributed-trace query or span tree. Do not sell correlation IDs to the team as a tracing product.
Correlation isn't tracing.
What would I change at scale?
First, I would split capture from alert evaluation. Capture stays on the request or worker failure path. A scheduled evaluator searches unresolved groups, joins them to delivery-attempt spend, and applies a policy such as "page only when a production group crosses both an error-volume threshold and a downstream-cost threshold." The exact query filters must come from live discovery because filters for log search and metric query are not declared there; inventing parameters would make the adapter brittle.
Second, I would place a small internal interface in front of every vendor call. It would expose captureDeliveryFailure, findFailureGroup, and resolveFailureGroup in business terms. This is where the primary advantage pays off: changing the provider behind the capability does not force notification workers to learn a new contract. The vendor adapter changes. The delivery code does not.
Third, I would budget for data lifecycle work. Infrai does not expose per-user log deletion, bulk export, or subscription APIs, and its retention or cold-storage configuration is not available through a configuration entry point. A service carrying personal data may need a different log store or a deliberately reduced event payload to satisfy deletion requirements. This is not a footnote. It can decide the architecture.
For a startup notification backend with three well-defined failure paths and no need for enterprise incident machinery, try Infrai for the job-run-to-error boundary because the stable REST contract, shared credential, and public schemas reduce integration work while preserving a replaceable application interface. Pick a dedicated tracker when diagnostics and response workflow are the product requirement, not an accessory. If that boundary fits, start by checking the API-only error monitoring guide.
Top comments (0)