It was 2:17 AM when my phone buzzed for the seventh time that night. Edge 5xx rate had spiked to 1.2% in Singapore, just enough to trip our static threshold, then dropped back to normal before I could even open the dashboard. By morning, my team had muted that entire alert channel. We’d joined the classic trap — so many false positives that we almost missed a real origin collapse the next week. If you’ve run CDN operations for any length of time, you’ve felt that same fatigue.
Why Your Static Thresholds Are Crying Wolf
Most teams start with fixed thresholds because they’re easy to reason about. Cache hit ratio below 85%? Alert. Origin 5xx above 1%? Alert. P95 TTFB over 300ms? Alert. I’ve set up those exact rules myself around 2014, and they worked when we had three PoPs and fairly predictable traffic. Once you scale to 50+ edges, mixed content profiles, and bursty video events, those static lines become noise generators. A 75% cache hit ratio might be catastrophic for your image-heavy product pages at 10 AM, but it’s completely normal for your API edge at 3 AM when purge rates are high.
The real problem isn’t just bad thresholds, it’s that they ignore context. CDN traffic has strong seasonality — daily, weekly, and event-driven. I once spent a week chasing a “degraded” TTFB alert in Frankfurt that fired every Monday morning. Turns out it wasn’t degradation at all; it was the weekly catalog update that warmed caches differently. Our alerting didn’t know what day it was. Without baselines that account for time-of-day and day-of-week, you’re essentially comparing apples to whatever fruit happens to be in season.
That’s where anomaly alerting earns its keep. Instead of asking “is this value above X?” we should be asking “is this value unexpected given what we’ve seen before in this same context?” That shift requires a proper pipeline underneath. If your CDN Log Aggregation Architecture: Collecting Billions of Events Without Dropping Data can’t give you reliable, time-bucketed metrics with low latency, any anomaly detection you build on top will wobble. You need clean, aggregated time series before you can detect anything meaningful.
# Don't do this: static threshold that fires constantly
ALERT CDNFiveHundredSpike
IF edge_5xx_rate > 0.01 FOR 2m
# Do this: dynamic, context-aware check (pseudo-PromQL + recording rule)
# EWMA baseline over same hour last 4 weeks, 3-sigma deviation
- record: edge:5xx_rate:ewma_4w_same_hour
expr: ewma_over_time(edge_5xx_rate[1h] offset 1w)
* 0.4 + ewma_over_time(edge_5xx_rate[1h] offset 2w) * 0.3
+ ewma_over_time(edge_5xx_rate[1h] offset 3w) * 0.2
+ ewma_over_time(edge_5xx_rate[1h] offset 4w) * 0.1
- alert: CDNFiveHundredAnomaly
expr: |
(
edge_5xx_rate - edge:5xx_rate:ewma_4w_same_hour
) / stddev_over_time(edge_5xx_rate[4w]) > 3
and edge_5xx_rate > 0.005 # absolute floor prevents alerting on tiny noise
for: 10m
labels:
severity: warning
annotations:
summary: "5xx rate is 3 sigma above seasonal baseline"
Building Baselines That Don't Forget Seasonality
We learned the hard way that a single global baseline doesn’t work for a distributed CDN. An edge in São Paulo behaves nothing like one in Tokyo, and even within a PoP, your traffic mix matters. I now build baselines per PoP, per content type, and per hour-of-week. It sounds like a lot of cardinality, but it’s the only way I’ve seen that reduces false positives without hiding real issues. Your CDN Real-Time Log Streaming Architecture: From Edge Events to Analytics in Seconds is critical here — if you’re aggregating on 5-minute delays, you can’t feed a responsive baseline. We pushed our aggregation to 30-second buckets with streaming percentiles, and alert quality improved overnight.
For intermediate setups, you don’t need a full ML platform to get good results. An exponentially weighted moving average (EWMA) with a seasonal component handles most CDN signals beautifully. We use a simple double EWMA: one for the short-term trend (last 2-3 hours) and one for the seasonal expectation (same hour over last 4 weeks). The alert fires only when the current value breaks away from both by a significant margin. This catches sustained shifts — like an origin slowly getting sick — while ignoring brief spikes from a single bad eyeball network.
One more thing we had to enforce was alert inhibition and de-duplication. You don’t need 20 separate alerts when your origin is down; you need one clear signal. I’ve seen teams burn out because a single origin failure spawned PoP-level 5xx alerts, cache miss alerts, and TTFB alerts all at once. Grouping by root cause and using dependency-aware inhibition cut our incident noise by about 60%. The same principle applies to log volume — if you’re considering CDN Log Sampling Strategies: Reducing Storage Costs Without Losing Signal, make sure your sampling doesn’t bias the baselines. We kept 100% sampling for error and latency signals and sampled only 10% for successful cache hits when building our models.
# alertmanager.yml - inhibit noisy downstream alerts when origin is the root cause
inhibit_rules:
- source_matchers:
- 'alertname="OriginUnhealthy"'
target_matchers:
- 'alertname=~"CDN.*Anomaly"'
equal: ['datacenter', 'service']
# don't suppress for more than 30m, so we catch secondary failures
- source_matchers:
- 'alertname="CDNPoPDown"'
target_matchers:
- 'alertname="CDNHighTTFBAnomaly"'
equal: ['pop']
# Example SLO-aware routing: only page for user-impacting anomalies
route:
group_by: ['pop', 'content_type']
group_wait: 30s
group_interval: 5m
repeat_interval: 2h
routes:
- matchers:
- 'severity="critical"'
- 'user_impact="true"'
receiver: 'pagerduty-cdn-oncall'
- matchers:
- 'severity="warning"'
receiver: 'slack-cdn-ops'
continue: false
Picking the Right Signals, Windows, and Sensitivity
Not every metric deserves an anomaly alert. I’ve made the mistake of alerting on everything we could measure, and the team just started ignoring the channel. These days we separate metrics into two buckets: user-impacting signals that can page, and operational signals that just create a ticket or dashboard highlight. For CDN monitoring alerts, that distinction is everything. A sudden drop in cache hit ratio is worth investigating, but it shouldn’t wake someone up unless it correlates with higher origin latency or 5xx. For proper status interpretation, we align our codes with the HTTP Working Group Specifications, and for front-end impact we cross-reference guidance from web.dev Articles on Core Web Vitals thresholds so our TTFB anomalies map to actual user pain.
Windowing is where most intermediate teams get tripped up. Too short, and you alert on every transient spike; too long, and you miss the first 15 minutes of an outage. We settled on dual windows for our most critical alerts: a fast 5-minute window that requires a large deviation (4 sigma) to fire quickly, and a slow 15-minute window that fires on a smaller but sustained deviation (2.5 sigma). The fast window catches hard failures — like a bad config push that breaks caching in one PoP — while the slow window catches gradual degradation, like an origin database warming up poorly. Both rules need to pass an absolute floor check so we don’t alert when error rates go from 0.001% to 0.003%.
Finally, tune sensitivity with real incident data, not gut feel. We replayed six months of edge telemetry through our proposed rules and measured precision and recall against our postmortem timeline. That backtest showed us we’d been far too sensitive on bandwidth anomalies and not sensitive enough on 5xx. Start with the table below as a baseline I’ve used across several media and e-commerce CDNs, then adjust based on your own traffic shape and page thresholds.
| Alert Signal | Baseline Window | Anomaly Threshold | Evaluation Window | Action |
|---|---|---|---|---|
| Edge 5xx Rate (per PoP) | Same hour, last 4 weeks (EWMA) | 3.0 sigma + absolute >0.5% | 5m (fast) / 15m (slow) | Page on-call |
| Cache Hit Ratio (per content type) | Rolling 7-day EWMA | 2.5 sigma below baseline | 15m | Ticket / Slack |
| p95 TTFB (per PoP) | Same hour, last 4 weeks | 2.0 sigma + >150ms over baseline | 10m | Page if correlated with 5xx |
| Origin Offload Error Rate | 1-hour EWMA | 3.5 sigma above baseline | 5m | Page on-call |
| Request Rate Delta (per PoP) | Same hour, last 4 weeks | 40% drop vs expected | 10m | Ticket, possible PoP drain |
Putting It Into Production: From Baseline to Pager
Theory's nice, but an alerting pipeline lives or dies on how you wire it up. We've learned the hard way: don't try to run anomaly detection on raw log lines. Aggregate first. Our typical stack is pretty boring — and that's the point. We tail CDN access logs and edge metrics into Kafka, 5-minute tumbling windows in Flink or ClickHouse materialized views, then roll up by PoP, cache status, and origin. You want two levels of aggregation: global for "is the internet broken?" and per-PoP / per-region for "is Ashburn melting?" If you only alert globally, you'll miss a 100% error spike in a single PoP that's still only 2% of global traffic.
Once you've got those 5-minute buckets, compute baselines separately and keep them fast. Don't query 8 weeks of history on every alert evaluation — pre-compute it. Every hour, we calculate the expected value and standard deviation for each signal sliced by hour-of-week (that's 168 buckets) using the last 6 weeks, excluding incident windows. Store that in Redis or Mimir as your forecast. Then your real-time check is just: actual vs. expected. For most CDN signals like cache hit ratio, origin latency p95, and 5xx rate, a robust z-score (using median and MAD, not mean) with a 3.5-sigma threshold beats static thresholds by a mile. You'll cut noise by 60-70% on day one. And if you're already aggregating these logs, you're sitting on behavioral gold — the same pipeline powers Clickstream Analysis at CDN Edge: Understanding User Navigation Patterns without adding new collection.
Sensitivity isn't one knob, it's three. We run every signal through a two-stage gate. Stage one: is it statistically anomalous? Stage two: is it operationally relevant? A 4-sigma spike in 4xx at 3 AM that affects 12 requests per minute shouldn't wake anyone up, even if it's anomalous. We enforce a minimum volume floor — e.g., don't fire on error rate unless you had >500 req/min in that window — and a persistence check. One anomalous 5-minute bucket is a blip. Two consecutive buckets, or 3 out of 5, is an incident. That alone dropped our alert volume from ~45 alerts per day to 4-6 actionable ones.
# Simplified baseline-aware check (Python / PySpark style)
# Runs every 5 min per PoP + signal
import numpy as np
def is_anomalous(actual, baseline_median, baseline_mad, min_volume, current_volume):
if current_volume < min_volume:
return False # Not enough traffic to be meaningful
# Robust z-score using MAD (more resilient than stddev)
# 0.6745 is the 0.75th quantile of normal distribution
if baseline_mad == 0:
return False
robust_z = 0.6745 * (actual - baseline_median) / baseline_mad
# Require both statistical and operational significance
return abs(robust_z) > 3.5
# Example: p95 origin latency for iad-01
# baseline_median=85ms, baseline_mad=12ms, actual=142ms
# robust_z = 0.6745 * (142-85)/12 = 3.20 -> No alert (just under)
# actual=155ms -> robust_z = 3.93 -> Alert (if volume > 500 rpm)
Advanced Patterns: Correlation, Suppression, and Why Your Alerts Still Lie
Single-signal alerts will still fatigue your team, even with perfect baselines. The real win is correlation. A cache hit ratio drop alone might be a purge. A hit ratio drop *plus* a spike in origin p95 *plus* elevated 5xx from that same origin — that's a real origin overload. We bundle related signals into a "situation" before paging. In practice, that means your alert manager (Alertmanager, PagerDuty, whatever) shouldn't get 7 separate alerts for one event. We use parent-child suppression: if the global origin health check is failing, suppress the 12 downstream PoP-level latency alerts and send one consolidated page with context. That change alone cut our midnight pages by 40%.
Then you need to troubleshoot the liars. The top three culprits we see: deployments, seasonality shifts, and low-traffic ghosts. Did someone just push a bad cache key config? Correlate alerts with your deploy and purge API logs — auto-annotate the timeline and suppress for 10 minutes post-deploy unless the error rate jumps >5x baseline. Got a holiday? Your hour-of-week baseline learned that Thanksgiving traffic looks like a Sunday, but Black Friday looks like nothing it's ever seen. You need a separate holiday/exception calendar that switches to a "big event" profile with wider bands. And for low-traffic PoPs or new properties, your baselines are garbage for the first 14 days — don't alert on z-score there, alert on absolute thresholds until you've got enough history. This is especially important if you're using those same edge logs for User Journey Analytics from CDN Logs: Mapping the Path to Conversion or Conversion Attribution from CDN Edge: First-Party Tracking Without Client-Side Scripts, where sparse paths can look like anomalies but are just sampling noise.
Finally, close the loop. Every alert needs a one-click feedback button: "useful / noisy / missed incident." We feed that back monthly to auto-tune sigma thresholds per signal. If the team marked 8 of the last 10 "origin latency" alerts in SIN as noisy, we bump that PoP's threshold from 3.5 to 4.0 and require 3 consecutive windows instead of 2. It’s not machine learning magic — it’s just respecting operator feedback. Without that loop, your perfectly tuned system will slowly drift and you’ll be back to crying wolf in three months. We review our false positive rate weekly and aim to keep it under 15% — if we’re drowning in noise again, we know we tuned too hot.
Frequently Asked Questions
How many alerts per day should a healthy CDN alerting system generate?
For a mid-size CDN (50-200 PoPs, 1-10 Tbps), aim for 3-7 actionable alerts per day that require human triage, and fewer than 2 pages overnight per week. If you're seeing 30+ alerts daily, your thresholds are too sensitive or you're not correlating signals. We've found that more than 10 alerts per on-call shift leads to alert blindness, where engineers start ignoring the channel entirely. Track your signal-to-noise ratio — you want at least 70% of alerts to result in a ticket, investigation, or runbook action.
What window size works best for CDN anomaly detection?
5 minutes is the sweet spot for most CDN use cases. 1-minute windows are too noisy — you'll get spikes from normal bursty traffic and your baseline variance explodes. 15-minute windows are too slow — you'll be 10 minutes late to catch an origin failure that's already impacting users. We use 5-minute evaluation windows with a 2-of-3 persistence rule, but compute baselines on 1-hour buckets to capture seasonality cleanly. For ultra-critical signals like global 5xx rate, we run a parallel 1-minute fast-path with a much higher threshold (5-sigma) just for early warning.
Can I use the same baselines for cache hit ratio and origin latency?
No — they behave completely differently. Cache hit ratio is highly seasonal and bounded (0-100%), so use hour-of-week medians with MAD and expect 2-4% normal swings. Origin latency is heavy-tailed and spiky, so p95 is more stable than mean, and you should use log-transformed values before calculating z-scores. Error rates (5xx) are sparse and Poisson-like; don’t use z-scores at all there — use a Poisson or negative binomial model, or simply alert on rate > 0.5% with >3x baseline. One-size-fits-all baselines are why static thresholds fail in the first place.
How do you handle auto-scaling and traffic shifts without false positives?
Normalize first. Never alert on raw request counts or bandwidth — alert on ratios and percentiles. When auto-scaling adds 20 new edge nodes, raw traffic per node drops, but cache hit ratio and error rate shouldn't. We also tie our baseline selector to traffic volume: if current traffic is 3x the baseline median (like during a flash sale or DDoS), we widen the sigma bands to 4.5 and require a higher absolute impact. And always exclude the 30 minutes before and after a scaling event from future baseline training, or you'll bake the anomaly into next week's "normal."