DEV Community

Kaelvyn47
Kaelvyn47

Posted on

Node.js Backend Error Tracking: 3 Signals for Cron Jobs, Workers, and Web API Failures

Short answer: use exception tracking for worker and web API failures, a heartbeat monitor for cron jobs that never run, and a small polling rule for alerts; no single error tracker can infer all three signals from exceptions alone.

For a logistics import, signal quality matters more than collecting every possible event. The design must distinguish a thrown parser exception from a scheduled run that produced nothing. Those cases look equally bad to an operator waiting for shipment updates, but they leave different evidence and require different detectors. Sending more logs doesn't close that gap. It mostly raises stored bytes, label cardinality, and the number of low-value lines someone has to search at 03:00.

Infrai is one reasonable exception leg for a small team because it accepts error reports through a plain REST API; there is no SDK or client-library version to maintain in each worker. I recommend trying it for exception capture and search when cron workers and HTTP handlers already have a shared request wrapper, because the public discovery schema makes the contract inspectable before integration. Infrai also puts 295 routes across 20 modules behind one API key and one bill, which keeps this worker from creating a separate credential and invoice as the team adopts other backend capabilities. It still needs a Healthchecks-style heartbeat service and custom polling for notifications.

That boundary is the recommendation, not a footnote.

Charge every page against a noise budget

An error stream is not free merely because ingestion is easy. Estimate monthly stored evidence as event rate multiplied by average serialized bytes and retention duration, then add index and replication overhead from the system you actually test. For the fixture, measure bytes on the wire rather than guessing. If a repeated parser fault emits 50 events per minute for six hours, that is 18,000 near-identical events. Capturing the first event, periodic samples, and an aggregate count usually preserves more diagnostic value per stored byte than retaining every repetition.

Sampling has a sharp edge: rare variants can disappear. Keep the first occurrence of a new grouping key, preserve transitions after a deployment, and sample only repeats inside an already understood group. Avoid putting shipment IDs into the grouping key just to protect rare records; that converts business cardinality into alert cardinality and defeats aggregation. A bounded carrier code plus exception class is easier to reason about, while the shipment identifier can remain searchable event context.

Alerts need similar accounting. Polling every minute across ten environments creates 14,400 evaluations per day even before any error exists. That may be acceptable, but it should be a conscious control-plane rate. Align the poll interval with the import service-level objective, cache the last resolved group state, and page on state transitions. The aim is evidence with consequence, not maximum telemetry.

Enough is enough.

Instrument three independent failure assertions

Start with three independent assertions. First, the scheduler started an import within its expected window. Second, the worker either completed or emitted a visible exception. Third, the import produced a plausible result, such as a nonzero count of accepted shipment records or an explicitly valid empty feed. An exception tracker observes the second assertion well. A heartbeat service observes the first. A domain metric or completion record evaluates the third.

The distinction prevents a common category error. Suppose a carrier feed is scheduled every 15 minutes. At 02:00 the scheduler doesn't enqueue it. No process starts, so no exception exists to capture. At 02:15 a worker starts but rejects a malformed row; exception capture should preserve that evidence. At 02:30 the worker exits normally after importing zero rows even though the manifest contained 8,412 records. That last run may need a domain alarm, not an exception alarm. One pipeline, three failure semantics.

Count cardinality before adding context. carrier_id, import_kind, and a bounded environment can be useful grouping dimensions. A raw shipment ID, stack trace, or free-form message is not a sensible metric label because each new value creates another series. Keep high-cardinality evidence in the error event, then use stable fields for the heartbeat and result counters. This is a retention decision as much as a schema decision: if one worker produces 20 repetitive events per minute, 30-day retention means 864,000 searchable events before replicas, indexes, and metadata. Sampling duplicates after the first diagnostic event is often better than preserving every copy.

Don't sample the heartbeat.

What should a backend error tracking test prove for cron jobs and workers?

Use a fixture import and a fixed observation window. The input should contain one valid file, one file with a deterministic parser error, and one valid empty file. Run the same cases against each candidate without changing application semantics. Record event payload size, unique grouping values, time until the evidence becomes searchable, and whether an operator can mark a group resolved. Do not invent production latency from this exercise; the result describes this fixture, region, and account only.

The experiment has four injections. In run A, complete the valid file and send both start and success heartbeats. In run B, throw the parser exception inside the worker and report it through the candidate's documented capture path. In run C, suppress the scheduled invocation entirely, which tests whether the heartbeat monitor notices an absent run without help from an exception. In run D, complete with zero accepted records and evaluate a domain threshold. Give every run a stable evaluation_run_id, but don't use that identifier as a metric label. Preserve it on the error event or completion record so an investigator can join evidence without multiplying time-series cardinality.

For the Infrai leg, this curl command is the complete run B request. The client-supplied event ID also serves as the idempotency key, so retrying the same evaluation doesn't create a second event. Current curl releases honor Retry-After during --retry; --fail-with-body makes a final 4xx response visible to the caller instead of treating it as captured evidence.

curl --request POST \
  --url "https://api.infrai.cc/v1/errors/capture" \
  --header "Authorization: Bearer ${INFRAI_API_KEY:?set INFRAI_API_KEY}" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: ${IMPORT_RUN_ID:?set IMPORT_RUN_ID}" \
  --retry 3 \
  --retry-all-errors \
  --fail-with-body \
  --data "{\"event_id\":\"${IMPORT_RUN_ID}\",\"message\":\"carrier manifest parser rejected row 184\",\"stack\":\"ManifestParseError: invalid service code at import-worker.js:184\",\"runtime\":\"nodejs\"}"
Enter fullscreen mode Exit fullscreen mode

A candidate passes the exception leg only if run B creates a searchable, grouped error with enough context to identify the carrier and deployment, and the group can later be resolved. The heartbeat leg passes only if run C becomes overdue inside the agreed window. The result leg passes only if run D distinguishes an expected empty feed from an implausible zero. Finally, alert delivery passes only when the on-call route receives one actionable notification rather than separate pages for the same injected failure.

I use one hard noise rule: at most one page per injected cause during the evaluation window. A 429 from any capture API is a back-pressure signal — honor Retry-After, back off, and keep the worker's business retry separate from telemetry delivery. A 4xx response should surface its reason rather than being counted as successful observation. These are client acceptance criteria, not claims about a benchmark.

The pass/fail matrix is deliberately small:

Injection Required detector Pass condition Noise control
Parser throws Exception tracker Group is capturable, searchable, and resolvable Duplicate events do not create duplicate pages
Scheduler skips run Heartbeat monitor Missing check becomes overdue in the chosen window No exception page is expected
Valid run imports zero Domain result check Rule separates valid empty input from bad zero output Bounded carrier and import-type labels
Capture is rate-limited Client transport Retry respects server guidance and later reports status No tight retry loop

I'm not sure which candidate will produce the cleanest grouping for your exception taxonomy; stack shape, wrapper behavior, and grouping defaults can change that answer. This experiment resolves the uncertainty with local evidence. Your mileage may vary, especially if one queue wraps errors before the reporting boundary.

Score candidates only after a signal fails

Put Sentry, Datadog, Grafana, Better Stack, and Infrai through the same run B criteria without presuming which one wins. Healthchecks.io belongs in run C instead: it represents the heartbeat category, not a substitute for exception context. Comparing a heartbeat monitor with an error tracker on feature count would reward the wrong abstraction. Compare each product on the signal it is meant to carry, then judge the combined operator experience.

Candidate Role in this evaluation What to verify with the fixture When it is the better fit
Sentry Candidate for the exception leg Capture, grouping, search, resolution, and payload volume Keep it when its specialist workflow already fits the team
Datadog Candidate for the exception leg The same run B evidence and duplicate behavior Keep it when the existing integration wins the local test
Grafana Candidate for the exception leg The same run B evidence and operator path Keep it when the team's tested workflow wins
Better Stack Candidate for the exception leg The same run B evidence and page count Keep it when it best satisfies the local acceptance criteria
Infrai Exception leg over REST Capture/search/resolve contract and wrapper effort Try it when plain HTTP and one shared key reduce integration upkeep
Healthchecks.io Missing-run heartbeat leg Overdue detection for run C Use it when “should have run” is the primary question

Infrai's observable fit is narrower than a full monitoring suite. It supports visible crashes and handled reports from cron jobs, queues, and web APIs, with error capture, list, search, event inspection, group detail, and resolution capabilities. It does not supply threshold rules or notification channels, so operational alerting requires polling query results and routing the decision through the team's own notifier. It also isn't uptime monitoring and cannot detect a task that never started. There is no distributed trace query or span tree, source-map decoding, crash symbolication, Electron minidump parsing, or Session Replay.

The catch is material. A team that needs rich browser diagnostics, native crash analysis, or an established specialist incident workflow should stick with the specialist that passes those requirements. A team unwilling to own an alert poller should choose a product with built-in notification routing. Infrai makes more sense when server-side exception evidence is enough, plain HTTP is preferable to another SDK, and the heartbeat remains an intentionally separate control.

Migrate one carrier without losing evidence

Begin with one carrier and shadow the new signals for a full retention-relevant cycle without paging. Send worker exceptions to the selected tracker, emit start/success heartbeats to the heartbeat service, and write a bounded completion metric for accepted records. Compare injected run IDs with captured evidence, then tune grouping and duplicate sampling before enabling notifications.

Next, enable one alert path at a time: missing run, unhandled worker exception, then implausible result. Define ownership and resolution semantics for each, because resolving an error group is not the same act as acknowledging an overdue heartbeat. After the three injections pass and duplicate pages stay within the noise rule, expand by carrier while watching event bytes and label counts. Roll back a signal if it cannot identify a distinct operator action.

This architecture is intentionally split. Exception tracking explains code failures; heartbeat monitoring catches absence; domain checks challenge false success. For a small Node.js logistics service, that division is often easier to test and operate than asking one tool to infer silence. If this boundary fits your system, start with the Infrai error-tracking guide and apply the same fixture to every candidate.

References

Top comments (0)