Telemetry — Anomaly Detection
Overview
RollingAnomalyDetector implements statistical anomaly detection using a rolling window of recent observations and a z-score calculation. It is designed for streaming metric data where the baseline shifts over time. Each metric tracked by the detector maintains its own independent window.
The detector is used internally by AIObservabilityWorker when an alert is configured with mode: 'anomaly', but it can also be used standalone to inspect anomaly results directly.
Key Concepts
- Rolling window — a FIFO buffer of the last
windowSizevalues for each metric. When the buffer is full, the oldest value is dropped before the new one is added. - Z-score — measures how many standard deviations a value is from the window mean:
zScore = (value - mean) / stdDev. A z-score of 0 means the value is exactly average. - Sensitivity — the z-score magnitude threshold. A value is flagged as an anomaly when
|zScore| >= sensitivity. minSamples— the minimum number of values the window must contain before the detector will flag anything. This prevents false positives on startup when the window is nearly empty and statistics are unreliable.
API Reference
RollingAnomalyDetector
import { RollingAnomalyDetector } from '@tagna/udiot/telemetry';
const detector = new RollingAnomalyDetector(options?);
Constructor options: AnomalyDetectionOptions
| Field | Type | Default | Description |
|---|---|---|---|
windowSize |
number |
50 |
Maximum number of samples retained per metric |
minSamples |
number |
8 |
Minimum samples required before anomalies are flagged |
sensitivity |
number |
2.5 |
Z-score threshold; values with ` |
Constructor options serve as instance defaults. They can be overridden per call via the options argument on .detect().
.detect(metricName, value, options?): AnomalyDetectionResult
Feed a new observation into the detector and receive a result.
| Parameter | Type | Description |
|---|---|---|
metricName |
string |
Identifies which rolling window to use |
value |
number |
The observed metric value |
options |
Partial<AnomalyDetectionOptions> |
Per-call overrides for windowSize, minSamples, sensitivity |
The value is added to the window before statistics are computed. If the window exceeds windowSize, the oldest entry is removed first.
.reset(metricName?): void
- Called with no argument: clears all windows for all metrics.
- Called with a
metricName: clears only that metric's window.
AnomalyDetectionResult
| Field | Type | Description |
|---|---|---|
metricName |
string |
The metric name passed to .detect() |
value |
number |
The observed value |
mean |
number |
Rolling window mean at the time of detection |
stdDev |
number |
Rolling window standard deviation |
zScore |
number |
(value - mean) / stdDev; 0 when stdDev === 0 |
sensitivity |
number |
The z-score threshold that was applied |
sampleSize |
number |
Number of values currently in the window (after adding this value) |
timestamp |
number |
Unix timestamp in ms when .detect() was called |
isAnomaly |
boolean |
true when `sampleSize >= minSamples && |
How the rolling window works
Initial window (windowSize=5): []
Feed: 10 → [10]
Feed: 11 → [10, 11]
Feed: 10 → [10, 11, 10]
Feed: 12 → [10, 11, 10, 12]
Feed: 11 → [10, 11, 10, 12, 11] ← window full
Feed: 13 → [11, 10, 12, 11, 13] ← 10 (oldest) dropped
The mean and stdDev are recalculated from scratch over the current window on every call — no incremental update is performed.
Z-score formula
mean = sum(window) / windowSize
stdDev = sqrt( sum( (x - mean)^2 ) / windowSize )
zScore = (value - mean) / stdDev
When stdDev === 0 (all values in the window are identical), zScore is set to 0 to avoid division-by-zero. A spike on a perfectly flat baseline will only be detected once a second distinct value enters the window.
Tuning sensitivity
| Sensitivity | Detection behavior |
|---|---|
2.0 |
More sensitive — flags values that are 2σ away from the mean. Higher false-positive rate. |
2.5 |
Default. Good general-purpose balance. |
3.0 |
Stricter — only flags clear outliers (3σ). Fewer false positives on noisy metrics. |
> 3.5 |
Very strict. Only catches extreme spikes. |
Reduce sensitivity if you are getting too many false positives on a naturally noisy metric. Raise it if genuine anomalies are being missed.
Example: detecting a memory spike
import { RollingAnomalyDetector } from '@tagna/udiot/telemetry';
const detector = new RollingAnomalyDetector({
windowSize: 50,
minSamples: 8,
sensitivity: 2.5,
});
// Simulate 50 normal memory readings (around 512 MB)
for (let i = 0; i < 50; i++) {
const normalValue = 512 + (Math.random() * 20 - 10); // 502–522 MB
const result = detector.detect('process.memory_mb', normalValue);
console.log(`value=${normalValue.toFixed(1)}, isAnomaly=${result.isAnomaly}`);
// isAnomaly: false for all normal readings
}
// Inject a spike
const spikeResult = detector.detect('process.memory_mb', 950);
console.log(spikeResult);
// {
// metricName: 'process.memory_mb',
// value: 950,
// mean: ~512,
// stdDev: ~6,
// zScore: ~73, ← far above sensitivity of 2.5
// isAnomaly: true,
// sampleSize: 50,
// sensitivity: 2.5,
// timestamp: 1711094400000
// }
Integration with AIObservabilityWorker
When an alert rule has mode: 'anomaly', AIObservabilityWorker calls anomalyDetector.detect(metricName, metricValue, alertOptions) on each evaluation cycle. The windowSize, minSamples, and sensitivity from AIAlertOptions are forwarded as per-call overrides. The full AnomalyDetectionResult is attached to the AlertNotification as the anomaly field.
You can inject a custom AnomalyDetector implementation into the worker:
import { AIObservabilityWorker, RollingAnomalyDetector } from '@tagna/udiot/telemetry';
const worker = new AIObservabilityWorker({
anomalyDetector: new RollingAnomalyDetector({
windowSize: 100,
minSamples: 20,
sensitivity: 3.0,
}),
});
Common Patterns
Per-metric windows — the detector automatically maintains a separate window for each unique metricName. You do not need to create separate detector instances per metric.
Resetting after deployment — call detector.reset() when you deploy a new version that significantly changes a metric's baseline. Without resetting, old values from before the deployment will remain in the window and may trigger spurious anomalies during the transition.
Testing — use a small windowSize (e.g., 5) and minSamples (e.g., 3) in unit tests so you can warm up the detector with a few values without needing 50 data points.
Pitfalls
isAnomalyis alwaysfalseuntil the window has at leastminSamplesentries. The result is still returned with validzScoreandmeanfields — you can use those for inspection even before the threshold is met.- The detector uses population variance (divides by
n, notn-1). For small windows this slightly underestimates the true variance. The defaultminSamples: 8mitigates this. - Values are stored in an in-process array. On process restart, the window is empty. If you need persistent baselines across restarts, you must implement your own
AnomalyDetectorbacked by a database or cache.