At 2:47 AM on Black Friday 2019, our origin fleet for a large retail customer flatlined. The CDN edge was fine — cache hit ratio was still 92% — but that remaining 8% of cache misses suddenly tripled in volume and crushed ten under-provisioned origins. We’d sized for average traffic, not for the reality of how CDN traffic patterns actually behave. It took us 25 minutes to manually add capacity, and by then we’d served a pile of 502s. That night is why I don’t trust static origin pools anymore.
Why Static Origin Pools Collapse Under Real CDN Traffic
Most teams size their origins for peak average load plus a little buffer, say 30%. It looks safe on a spreadsheet, but it completely ignores how CDN traffic hits origins. You don’t get smooth, predictable curves. You get long stretches of near-idle time punctuated by violent cache miss storms — a purge, a viral asset, a cache key change, or just a shift in traffic geography that suddenly bypasses your edge. I’ve watched a single “purge all” from a CMS integration turn a quiet 800 RPS origin tier into a 12,000 RPS thundering herd in under 15 seconds.
Static pools force you to choose between two bad options. Overprovision and you’re burning money 90% of the day to handle the 10% spike — I’ve audited origin clusters running at 12% CPU at midnight just so they wouldn’t fall over at noon. Underprovision and you rely on the CDN to shield you, which it can’t do when the traffic is uncacheable or when you’re dealing with shielding misses. And when origins start to queue, everything degrades fast: connection pools saturate, time-to-first-byte climbs, and your Origin Connection Pooling Optimization: Reducing TCP Overhead at CDN Scale work gets undone because new connections start competing for the same exhausted backends.
The other problem is autoscaling lag. Even if you have dynamic scaling configured, if your scaling signal is wrong, you’ll scale too late. Scaling on raw CPU or average request rate is too slow for origin work. By the time your 5-minute CPU average breaches 70%, your p95 origin latency has already tripled and the CDN has started marking you unhealthy. You need signals that lead traffic, not follow it. That’s where proper Origin Health Monitoring Patterns: Active Probes, Passive Checks, and Hybrid Approaches have to feed directly into your scaling decisions, not just your alerting.
# Example: A broken static scaling policy that reacts too late
# This ASG policy looks reasonable until a purge storm hits
AutoScalingGroup:
MinSize: 6
MaxSize: 20
HealthCheckGracePeriod: 300
ScalingPolicy:
Type: TargetTracking
TargetValue: 65.0
PredefinedMetric: ASGAverageCPUUtilization
Cooldown: 300 # 5 min cooldown = disaster during miss storm
EstimatedWarmup: 180
# Problem: 5-min CPU average + 300s cooldown means you're
# 8+ minutes behind a spike that peaks in 30 seconds.
# Origin queue depth is already at 4k before the first new instance launches.
Picking Scaling Signals That Match Your Traffic Patterns
After that Black Friday incident, we rebuilt our scaling around signals that actually predicted origin pressure. For CDN origins, the best signals aren’t infrastructure metrics at all — they’re CDN-aware application metrics. The three I’ve found most reliable are origin-facing RPS (requests that actually reach origin), active connection queue depth, and p95 origin response time. CPU is a lagging indicator; queue depth is a leading one. If your origin’s queue jumps from 20 to 400 in 10 seconds, you don’t need to wait for CPU to confirm there’s a problem.
You also have to account for different traffic patterns separately. A steady daily ramp is easy to scale for, but a cache purge, a shield miss, or a flash sale creates a completely different shape. We now classify patterns into three buckets: predictable diurnal waves, stochastic miss bursts, and sustained elevated baselines (like after a cache key migration). Each gets a different scaling response. Diurnal traffic can use scheduled scaling plus gentle target tracking. Miss bursts need instant, aggressive scale-out with no cooldown, driven by edge-reported miss rates. I’ve had good luck feeding the CDN’s `origin.requests.count` and `cache.miss.ratio` directly into our scaler via a custom metrics pipeline — when miss ratio jumps 5% above baseline, we pre-scale 30% before the origin even feels it.
Don’t forget scale-in either. Aggressive scale-in saves money but it’ll kill you if you’re not careful. We learned to never scale in during periods of high retry activity. If your Origin Retry Strategies for CDN Resilience: Backoff, Jitter, and Budget Limits are firing, that’s not low traffic — that’s failed traffic that’s about to be retried. Tying your scale-in gate to a low 5xx rate and low retry budget is critical. The HTTP Working Group Specifications (https://httpwg.org/specs/) are clear on how retries and 503 handling should behave, and web.dev Articles (https://web.dev/articles/) reinforce that server timing and responsiveness directly impact Core Web Vitals, so protecting origin latency during scale events isn’t optional.
// Custom scaler logic: scale on queue depth + miss ratio, not CPU
// Runs every 10s, pushes desired count to autoscaler
function calculateDesiredInstances(metrics) {
const { queueDepth, missRatio, p95LatencyMs, activeInstances } = metrics;
// Leading indicator: queue building up
if (queueDepth > 250 || p95LatencyMs > 800) {
// Aggressive scale-out, ignore cooldown
return Math.ceil(activeInstances * 1.5);
}
// Predictive indicator: CDN reporting miss spike
// Baseline missRatio is ~0.08, spike to 0.15 means incoming wave
if (missRatio > 0.13) {
const missPressure = (missRatio - 0.08) / 0.08;
return Math.ceil(activeInstances * (1 + missPressure));
}
// Conservative scale-in only when truly idle
if (queueDepth < 30 && p95LatencyMs < 300 && missRatio < 0.09) {
return Math.max(6, Math.floor(activeInstances * 0.9));
}
return activeInstances;
}
Right-Sizing Backends Without Overpaying or Starving Requests
Backend sizing isn’t just “how many instances” — it’s what size they are and how quickly they can become useful. I’ve made the mistake of using large, slow-to-boot instances because they looked cheaper per core. They’re not, when a cold start takes 4 minutes and you need capacity in 40 seconds. For origin autoscaling, we’ve shifted to smaller, faster-launching instances (or containers) that can warm up in under 60 seconds, even if we need more of them. The math works out because dynamic scaling lets you run fewer of them most of the time. You’re optimizing for time-to-ready, not just cost per vCPU.
You also need to set honest target thresholds. A lot of guides tell you to target 40-50% CPU for headroom, but origins limited by network and connection handling often saturate on connections long before CPU. In our Node.js and NGINX-based origins, we hit connection limits at around 55% CPU, while a Java-based image service would CPU-saturate at 70%. There’s no universal number. You have to load test your actual origin stack with realistic CDN-like traffic — high concurrency, keep-alive reuse, and large header sets — and find where p95 latency starts to curve upward. That’s your real ceiling, not a nice round CPU percentage.
The table below is based on numbers from three production origin tiers we’ve run in the last two years. They’re not theoretical — they’re what we actually set as autoscaling targets after load testing. Notice how the right metric and target changes completely based on the traffic pattern. If you’re just starting with origin autoscaling, use this as a baseline and tune from there.
| Traffic Pattern | Primary Scaling Signal | Target Threshold | Scale-Out Reaction Time | Typical Instance Size |
|---|---|---|---|---|
| Diurnal Ramp (predictable daily peak) | Origin RPS per Instance | 800 - 1,000 RPS | 60 - 90 seconds | 4 vCPU / 8GB RAM |
| Cache Miss Burst (purge / viral asset) | Connection Queue Depth | < 200 queued requests | 10 - 20 seconds (no cooldown) | 2 vCPU / 4GB RAM (fast boot) |
| Sustained API Traffic (uncacheable) | p95 Origin Latency | < 650ms p95 | 30 - 45 seconds | 8 vCPU / 16GB RAM |
| Shield Miss / Regional Failover | Active TCP Connections | < 3,500 conns / instance | 15 - 30 seconds | 4 vCPU / 8GB RAM |
Putting Autoscaling Into Production: From Signals to Scaling Actions
Reading the right signals is only half the battle — you’ve got to wire them into a scaler that doesn’t panic on every spike. We’ve seen teams hook origin CPU directly to an Auto Scaling Group and then wonder why they’re flapping 8 times in 10 minutes during a cache purge. Don't do that. You want your decision layer to sit one step removed from raw metrics, usually in your CDN's load balancer or origin shield tier. We buffer origin concurrency and p95 origin response time over a 60-second rolling window, and only trigger a scale-out when both breach threshold for two consecutive evaluation periods.
For most mid-tier origins (4-8 vCPU, 16GB RAM), our sweet spot is scaling at around 65-70% sustained CPU or when active origin connections per node cross 750-850. But time-based thresholds matter more than you’d think. A 30-second scale-out cooldown is too twitchy for CDN traffic — you’ll double your pool on a single TikTok-driven burst that’s gone in 45 seconds. We run a 90-second scale-out cooldown and a much longer 300-second scale-in cooldown. That asymmetry prevents the classic sawtooth where you scale in too fast and immediately need to scale out again, which just burns instances and spikes your 95th percentile latency by 150-300ms while new backends warm their connections.
Here’s a simplified version of what we run in production for an AWS-backed origin pool behind a shield. It uses custom CloudWatch metrics fed from the CDN (origin_concurrency and origin_p95) rather than vanilla EC2 CPU, which lags by 2-3 minutes:
# Terraform - origin autoscaling based on CDN-fed metrics
resource "aws_autoscaling_policy" "origin_scale_out" {
name = "origin-concurrency-scale-out"
policy_type = "StepScaling"
adjustment_type = "ChangeInCapacity"
autoscaling_group_name = aws_autoscaling_group.origin_pool.name
step_adjustment {
scaling_adjustment = 2
metric_interval_lower_bound = 0
metric_interval_upper_bound = 15
}
step_adjustment {
scaling_adjustment = 4
metric_interval_lower_bound = 15
}
}
resource "aws_cloudwatch_metric_alarm" "origin_concurrency_high" {
alarm_name = "origin-concurrency-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
period = 60
threshold = 800 # active requests per origin
alarm_actions = [aws_autoscaling_policy.origin_scale_out.arn]
metric_query {
id = "m1"
label = "p95 Origin Latency (ms)"
expression = "SELECT AVG(origin_p95) FROM CDN/Origin"
period = 60
}
# Cooldowns: 90s scale-out, 300s scale-in set on ASG
}
When Autoscaling Fails: Edge Cases, Hedging, and Cold Origins
The ugliest failure isn’t under-provisioning — it’s scaling perfectly and still timing out because your new origins aren’t actually ready. We’ve debugged pools where the ASG reported 6 healthy instances, but the CDN kept 502ing 12% of requests for 4 minutes. The culprit was stale TLS sessions and empty connection pools. New origins need 20-30 seconds to establish warm keepalives to your database and fill local caches. If you add them to the load balancer rotation instantly, you’ll hammer them with cold-start latency that’s 3-5x normal. Fix it with a warmup period (we use 45-60s lifecycle hooks) and slow-start in your load balancer. And don’t forget to verify your Origin SSL Verification at CDN Edges: Preventing Man-in-the-Middle Attacks still passes on fresh nodes — we’ve seen autoscaled AMIs with expired intermediate certs that passed health checks but failed real edge verification.
Another pattern that kills you at scale is synchronized thundering herds after a cache miss. Say you have a 30-second TTL on a popular JSON endpoint. When it expires, 2,000 edge PoPs hit your origin at once. Autoscaling can’t react in 200ms, so you need request coalescing at the shield plus a fallback. This is where Origin Request Hedging: Sending Parallel Requests to Cut Tail Latency saves you. Instead of waiting 1.2 seconds for a single overloaded origin to respond, we hedge by sending a second request to another origin after a 150ms delay and take the first response. It cuts our p99 by ~40% during scale-out events without needing to over-provision by 50%.
Finally, tune your timeouts to match your scaler’s reality. If your Origin Timeout Configuration Guide: Connect, Read, and Total Timeout Tuning sets a 5-second read timeout but your autoscaler takes 90 seconds to bring up new capacity, you’ll just amplify errors. We set connect timeouts aggressively low (1.5s) to fail fast to another origin, but keep read timeouts at 8-10s for autoscaling events. Log every timeout with origin ID and scaling state — if you see read timeouts clustering 30-60 seconds *after* a scale-out trigger, your cooldown is too short or your health check is marking nodes healthy too early. Back off, increase your stabilization window, and add circuit breaking at 50% error rate per origin.
Frequently Asked Questions
How quickly should my origin pool scale out during a CDN traffic spike?
Target 60-90 seconds from alarm to in-service. Faster than 45 seconds usually means you're reacting to noise and will over-scale by 30-40%. Use CDN-fed metrics (active connections, p95 latency) with 60-second periods and 2 evaluation periods, not raw EC2 CPU which lags 2-3 minutes. Pair it with shield-layer request coalescing so your origins have breathing room while new nodes warm up.
Should I use CPU, request count, or latency as the primary scaling signal?
Don't rely on CPU alone — it's a lagging indicator for CDN workloads. We use a composite: active origin concurrency per node (threshold ~750-850) AND p95 origin latency (threshold 500-700ms) over a 60s window. CPU at 65-70% is a good secondary guardrail, but latency tells you when users are actually suffering. For event-driven traffic, queue depth or edge-to-origin request rate is even better.
Why are new autoscaled origins returning 502s even though health checks pass?
This is almost always a cold-warm problem. Your health check hits /healthz in 20ms, but real requests need warm DB pools, TLS handshakes, and local caches. Add a 45-60 second lifecycle warmup hook, enable slow-start (ramp traffic over 60s), and ensure keepalive pools are pre-warmed. Also verify SSL verification and origin timeouts aren't stricter for new nodes than for old ones.
How do I prevent scale-in from causing another immediate scale-out?
Use asymmetric cooldowns: 90 seconds for scale-out, 300 seconds (5 minutes) for scale-in. Enable a stabilization window and never scale in by more than 25% of your pool at once. We also require that origin p95 stays under 400ms and concurrency stays under 500 per node for 5 continuous minutes before allowing a scale-in. It feels conservative, but it eliminates flapping and saves you from that painful double latency spike.