Short answer: in NestJS, use an exception filter and interceptor for HTTP error tracking, add explicit capture boundaries around cron jobs and queue workers, and page only when a scheduled import has stopped producing results.
That distinction matters in logistics. An import can throw loudly, retry, and still deliver every expected record. It can also stop quietly after a scheduler change and emit no exception at all. Treating every captured error as an alert maximizes noise, not coverage.
The practical design is one event contract across HTTP requests, scheduled invocations, queue consumers, and the final import outcome. Keep the capture hooks local to those boundaries. Centralize normalization, cardinality limits, retention, and alert evaluation.
What signal proves that a scheduled logistics import stopped?
The strongest signal is a missing outcome, evaluated against an explicit schedule. For each import definition, record an invocation and then exactly one terminal outcome: succeeded, failed, or exhausted. The alert evaluator asks whether the latest successful outcome is older than the allowed interval plus a grace period. It doesn't ask how many stack traces arrived.
Use four signals, each with a different job:
- An HTTP exception event explains a rejected manual trigger or callback.
- A cron invocation event proves the schedule fired.
- A queue-consumer exception event explains an individual processing failure.
- An import outcome event proves whether useful work completed.
The fourth signal pages people. The first three support diagnosis.
Suppose depot D17 expects a carrier-manifest import every 15 minutes, allows 5 minutes of arrival jitter, and usually finishes within 4 minutes. An evaluation at 10:25 should alert if no successful outcome has appeared since 10:01, after accounting for the schedule, jitter, and runtime budget. Those values are configuration for this example, not universal defaults. A nightly customs feed needs a different window, and I'm not sure any fixed multiplier works across both; the evidence needed to set it is the observed arrival and completion distribution for each import class.
This outcome-first rule also catches the awkward cases: the scheduler never invoked the job, the queue accepted no work, a worker exited before its normal error path, or an upstream response was syntactically valid but produced zero accepted records. Exceptions alone can't establish absence.
How should NestJS error tracking connect HTTP exceptions, cron jobs, and queue workers?
Connect them through a small, transport-neutral capture function, not by trying to stretch one framework hook over every execution model. An HTTP filter owns uncaught request exceptions. An interceptor is useful for request duration and correlation, but it isn't the universal exception boundary. A scheduled method needs its own boundary, and a queue worker needs another around message processing. Each adapter maps local context into the same event shape.
The shape should contain stable fields: event time, execution kind, operation name, outcome, error class, and a correlation identifier. Logistics context such as import_kind or carrier_code is useful when its value set is controlled. Raw shipment IDs, filenames, URLs, exception messages, and stack traces don't belong in metric labels. They create a new series for nearly every failure — expensive to retain and poor for aggregation. Put high-detail values in sampled event payloads with shorter retention instead.
A generic intake contract can be exercised without binding the design to an SDK:
curl --request POST https://telemetry.example.test/v1/errors/capture \
--header 'content-type: application/json' \
--data '{
"occurred_at": "2026-08-15T02:20:00Z",
"execution_kind": "queue_worker",
"operation": "carrier_manifest_import",
"outcome": "failed",
"error_class": "ImportValidationError",
"correlation_id": "run_01",
"dimensions": {
"depot": "D17",
"carrier": "carrier_a"
}
}'
This is an illustrative contract on a pseudonymous endpoint, not a claim about a public API. In production, authentication and authorization still need to be defined, and the receiver should reject unknown dimension keys rather than silently admitting unbounded labels.
Normalize once. Don't normalize differently in the filter, scheduled task, and consumer, because TimeoutError, timeout, and ETIMEDOUT then become three operational categories even when the response policy is identical. Preserve the original exception detail in the event body where access can be restricted; use a bounded taxonomy for aggregation.
There is also a subtle ownership rule. A failure should be captured once at the boundary that can decide its outcome. If a queue consumer records an exception and rethrows it for retry, the outer queue adapter must update the attempt state rather than create an unrelated second error event. Otherwise one failure with three attempts turns into four apparent incidents. Count attempts and terminal failures separately.
Cardinality and retention are part of correctness
Telemetry cost is a data-model property before it is a vendor invoice. If a metric has 6 execution kinds, 20 operation names, 3 outcomes, 40 depots, and 12 carriers, the upper bound is 172,800 label combinations before environment or error class is added. Multiplying by 30 error classes reaches 5,184,000 potential series. Most combinations may never occur, yet the arithmetic exposes how a harmless-looking label can dominate storage and query work.
Keep alert labels brutally small.
For the missing-result alert, environment, operation, and perhaps depot may be enough. Carrier can remain an event attribute if nobody routes or groups alerts by carrier. Correlation IDs must never be labels. This isn't merely a cost preference: high-cardinality alert inputs make baselines sparse, fragment related failures, and turn one operational condition into many notifications.
Retention should follow the question each data class answers. A compact outcome series may need enough history to cover seasonal schedules and establish lateness. Full stack traces usually serve recent diagnosis, so a shorter window can be defensible. Audit records may have a separate policy driven by legal or business requirements. Don't copy one retention period across all three just because the storage control exposes one convenient default.
Sampling needs the same separation. Never sample the terminal outcome used to detect a stopped import. Sampling detailed, repeated exception events can be reasonable after the first event for a run is retained, provided counters preserve attempt and error totals. The catch is that tail sampling based only on duration can discard fast validation failures, while uniform sampling can miss a low-volume carrier entirely. Stratify by bounded error class or import kind only when the extra cardinality has an operational decision attached to it.
No free lunch.
Separate capture, alerting, and rollout
Capture answers “what happened?” Alerting answers “does a human need to act now?” Those are separate control loops. A production capture path should be able to accept repeated worker failures without paging, while the alert evaluator groups them under one overdue import. Conversely, the evaluator must page on missing outcomes even when capture has received no exceptions.
A compact policy table keeps that distinction reviewable:
| Condition | Store | Alert | Reason |
|---|---|---|---|
| HTTP trigger rejected | One exception event | Usually no | The caller can retry or correct input |
| Worker attempt failed, retry remains | Attempt count plus sampled detail | No | Work may still complete |
| Attempts exhausted | Terminal outcome plus error detail | Maybe | Urgency depends on the import deadline |
| No success beyond schedule and grace | Outcome timestamp | Yes | The business result is absent |
| Success after earlier failures | Outcome plus attempt count | No new page | Preserve reliability evidence without noise |
“Usually” and “maybe” are deliberate. A rejected security callback or a deadline-critical exhausted job can deserve immediate action, but that choice belongs in policy, not in the exception adapter.
Roll out the policy behind a feature toggle so shadow evaluation can compare proposed notifications with actual import outcomes before anyone is paged. Feature toggles carry carrying cost and should have owners and removal conditions; leaving every experiment in place makes runtime behavior harder to reason about. During shadow mode, record would_alert decisions without sending notifications, inspect false positives and false negatives, then enable routing by a stable cohort such as depot or import kind. An experimentation platform can manage controlled allocation, but the architecture shouldn't require one: a reviewed configuration file can express the same staged cohort for a small system.
This method is not suitable when an import has no declared schedule or externally observable completion condition. In that case, “stopped producing results” has no testable definition. Establish a service-level expectation first. Stick with direct exception paging only for low-volume, human-invoked work where every failure requires immediate action and retries cannot mask the result.
A compact production rollout
Start with one import class and enumerate its four boundaries. Add the normalized event contract, reject uncontrolled dimensions, and verify that one logical failure produces one terminal outcome even across retries. Then run the overdue evaluator in shadow mode for at least several complete schedule cycles; the required duration depends on the slowest legitimate cadence, so a universal number would be misleading.
Next, calculate the maximum label combinations before enabling each dimension. Set separate retention for outcome aggregates and detailed exception events. Exercise four cases through deployment: a rejected HTTP trigger, a scheduled invocation that queues work, a worker retry that later succeeds, and an invocation that never produces a successful outcome. The last case must generate one grouped alert after its grace window. The retry case must not.
Finally, enable routing for a narrow, stable cohort and attach every notification to the latest invocation, terminal outcome, and sampled diagnostic events. Remove the rollout toggle after the policy is accepted. The system is finished when an operator can answer three questions from one alert: which expected import is missing, when it last succeeded, and which boundary last reported progress.
Top comments (0)