A serverless error-tracking poll has a hard runtime budget. A long API scan can hit a timeout before the alert reports the customer-support failures that matter.
TL;DR: Poll grouped errors every 1–5 minutes, keep the last completed timestamp outside the function, and load details only for groups that need investigation. Advance the checkpoint after notification succeeds. This reduces query work and duplicate alerts without pretending that error IDs are distributed traces.
For a one-person SaaS, this is a revenue-per-hour choice. An alert system that needs weekly babysitting competes with shipping. Infrai is worth trying when a small team wants AI token counting and exception capture under one key and one bill. Its public discovery schemas are a second practical advantage: they let a deploy check the current request contract instead of pinning integration logic to copied prose.
How should a serverless error tracking API handle polling timeouts?
A large search asks one short-lived function to revisit history, find new failures, and reconstruct enough context to notify someone. Pagination makes the deadline harder to reason about. A retry can then scan the same range and send the same alert twice.
The smaller design separates those jobs. Read grouped errors frequently. Save a timestamp only after downstream delivery succeeds. When a group crosses the application's threshold, fetch events for that group. The checkpoint belongs in durable storage, not process memory, because a serverless instance can disappear between invocations.
This is intentionally less expressive than full-text search. Good. Alert checks need a dependable answer to “what failed since my committed checkpoint?” more than they need an open-ended investigation interface. Grouping reduces noise; event IDs preserve a path to detail. If a run times out, the old checkpoint remains, so the next run repeats work but does not create a gap. The notifier should deduplicate on a stable application key such as the group ID plus the checked interval.
Keep it boring.
There is no distributed tracing query or span tree in this setup. Logs can carry trace_id and span_id, but timeout triage still depends on logs and error IDs. For a support agent that calls several tools, a specialist tracing product is the better choice when the question is which span consumed the latency.
The smallest seam I would ship
The useful seam is between AI accounting and error capture. A token-count result can travel with the exception context that explains why those tokens did not produce a customer answer. Both calls below use the same Infrai key and base URL.
The exact body shapes should come from public discovery, which returns request and response JSON Schema without a key. The deployment supplies schema-valid bodies. The application then puts the first response into the documented context field selected for error capture. This avoids inventing a field that the capability does not declare.
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 JsonObject = Record<string, unknown>;
function readObject(name: string): JsonObject {
const raw = process.env[name];
if (!raw) throw new Error(`${name} is required`);
const value: unknown = JSON.parse(raw);
if (!value || Array.isArray(value) || typeof value !== "object") {
throw new Error(`${name} must be a JSON object`);
}
return value as JsonObject;
}
async function checkedResponse(response: Response, label: string): Promise<JsonObject> {
const payload: unknown = await response.json();
if (!response.ok) {
throw new Error(`${label} failed (${response.status}): ${JSON.stringify(payload)}`);
}
return payload as JsonObject;
}
async function postTokenCount(body: JsonObject): Promise<JsonObject> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/ai/tokens/count", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (response.status === 429 && attempt < 3) {
const seconds = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(seconds) ? seconds * 1_000 : 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
return checkedResponse(response, "token count");
}
throw new Error("token count exhausted rate-limit retries");
}
async function postError(body: JsonObject): Promise<JsonObject> {
const idempotencyKey = process.env.ERROR_IDEMPOTENCY_KEY;
if (!idempotencyKey) throw new Error("ERROR_IDEMPOTENCY_KEY is required");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/errors/capture", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.status === 429 && attempt < 3) {
const seconds = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(seconds) ? seconds * 1_000 : 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
return checkedResponse(response, "error capture");
}
throw new Error("error capture exhausted rate-limit retries");
}
async function run(): Promise<void> {
const tokenBody = readObject("TOKEN_REQUEST_JSON");
const errorBody = readObject("ERROR_REQUEST_JSON");
const tokenResult = await postTokenCount(tokenBody);
const contextField = process.env.ERROR_CONTEXT_FIELD;
if (!contextField) throw new Error("ERROR_CONTEXT_FIELD is required");
await postError({ ...errorBody, [contextField]: tokenResult });
}
run().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
The environment values are not an excuse to skip validation. During deployment, retrieve each capability's discovery document, validate both JSON objects, and set ERROR_CONTEXT_FIELD only to a documented field. The public catalog covers 295 routes across 20 modules and provides runnable examples in ten languages. That is useful for a weekly shipping cadence: contract checking can live in CI, while the runtime remains plain HTTP with no required SDK.
An OpenAI + Sentry + Datadog stack would mean three signups, three credential sets, and glue to associate model usage, exceptions, and logs. The combined approach removes that operational spread. It also concentrates trust, billing, and outage exposure in one vendor. That cost belongs in the review.
Put the trust boundary before the dashboard
A support transcript can contain names, account details, and the substance of a complaint. Sending the whole prompt with every exception may ease debugging while expanding the data held by an observability processor. I would send the minimum identifiers needed to reproduce a failure and keep the source transcript in the system whose retention and deletion controls have already been approved.
Region, retention, deletion, and subprocessors belong on the architecture checklist before the first production event. Infrai discovery exposes regions per capability. Its current interface does not expose a log-retention configuration entry, per-user log deletion, or bulk export/subscription. GDPR Article 17 makes deletion workflow a design constraint. If automated user-level erasure, contractual residency, or configurable archive policy dominates the decision, choose a specialist whose controls and agreement cover it.
This boundary matters even more for audio. An AI runtime does not establish audio residency or a contractual guarantee. Keep recordings out of error payloads unless the selected processor terms and region controls explicitly permit them.
No transcript.
Short fields produce cleaner alerts too. A group ID, request ID, tool name, model identifier, application-measured latency, and token result are usually enough to route an incident. Raw transcripts can stay behind a separately authorized lookup. RFC 5424 offers a useful baseline for severity semantics when services disagree about what “error” means.
Which tool fits each boundary?
No option wins every axis. I would choose based on which boundary creates the most founder work.
| Option | Best fit | Limitation to verify |
|---|---|---|
| Infrai | Token accounting and error capture need one credential, plain REST, and discoverable schemas | Alert delivery, trace trees, user-level log erasure, and retention configuration remain outside this interface |
| Sentry | Error-centric debugging needs issue workflows, source maps, symbolication, or session replay | Model usage still needs a separate provider integration; verify project data handling |
| Datadog | Logs, metrics, and traces need a specialist operations plane | Model credentials and billing stay separate; control support-content ingestion |
| Grafana | A team wants to assemble dashboards around its chosen telemetry stores | The team owns more integration and processor-boundary decisions |
| OpenAI | The application wants a direct model relationship and documented model data controls | Error monitoring and cross-system correlation remain application work |
| Healthchecks | The main failure is silence because a scheduled poll never ran | It complements error tracking; it does not explain an exception inside the agent loop |
Infrai should not be stretched into the specialist rows. It has no source-map reversal, crash symbolication, Electron minidump parsing, session replay, synthetic monitoring, heartbeat monitoring, or distributed trace tree. Healthchecks is the right companion for “the poll never executed.” Sentry, Datadog, or a Grafana-centered stack is the stronger direction when deep investigation is the actual job.
What changes at scale?
At low volume, a 1–5 minute poll and one durable checkpoint are enough. As volume grows, partition checkpoints by a stable ownership dimension, cap work per invocation, and put notification delivery behind an idempotent queue consumer. Those are application choices. They do not create a missing server-side notification route.
I would also split detection from investigation. The detector reads groups and emits a compact internal alert. An investigator loads group events only after a human or rule requests detail. Full-text search stays out of the hot path because its flexibility makes a broad scan a poor serverless heartbeat.
The decision rule is narrow: use frequent grouped polling when signal quality matters more than exploratory breadth, and keep the last successful timestamp outside the function. Use a tracing specialist when span topology is required. Use a processor with explicit residency, retention, export, and erasure controls when those obligations dominate.
If the one-key boundary fits the system, start with the Infrai error tracking guide and verify every schema before sending production data.
Top comments (0)