Cloud Architecture

How Do Cloud Platforms Handle Black Friday Traffic?

Learn how Kubernetes HPA handles traffic spikes, cold starts, scaling flaps, and database connection limits during production load surges.

TechViz Hub

Conceptual diagram of Kubernetes auto-scaling under Black Friday traffic load

Why this matters

At 6:00 AM on Black Friday, your e-commerce platform is humming along at 200 requests per second across 3 pods. By 6:01 AM, a flash sale link hits Twitter and traffic rockets to 1,800 rps. Your pods peg at 95% CPU.

Requests start dropping. The Horizontal Pod Autoscaler notices — but the cooldown timer hasn’t expired yet. For 30 agonizing seconds, customers see 502 errors.

This is the core tension of auto-scaling: react too fast and you get scaling flapping — pods thrashing up and down every few seconds, burning money and destabilizing your cluster. React too slow and you shed traffic during the most revenue-critical minutes of the year.

Kubernetes HPA is the industry standard for horizontal scaling, but its default settings are designed for stability, not speed. Understanding the formula it uses, the cooldowns that gate its decisions, and the pod lifecycle delays that separate “desired” from “actual” replicas is the difference between a smooth Black Friday and a postmortem.

My rule of thumb Autoscaling is capacity planning with a delay, not magic elasticity. I care as much about startup time and connection budgets as I do about the HPA formula.

The mental model

HPA runs a control loop every 15 seconds (by default) that calculates the desired number of replicas using a simple formula:

desiredReplicas = ceil(currentReplicas × (currentCPU / targetCPU))

If you have 3 pods averaging 85% CPU and your target is 50%, HPA computes ceil(3 × 85/50) = ceil(5.1) = 6. It wants 6 pods. But it won’t act immediately — it checks whether the scale-up cooldown has expired since the last scaling event.

Once HPA decides to scale up, Kubernetes creates new pods. These pods don’t serve traffic instantly. They go through a lifecycle: Pending → ContainerCreating → Running (but not Ready) → Ready. Only Ready pods receive traffic from the Service.

If your container image is 800MB and your init containers run database migrations, that startup time might be 30–60 seconds. During this window, your existing pods are still overloaded. The traffic is distributed round-robin across Running pods only, and each pod can handle approximately 100 rps at 100% CPU. If a pod’s CPU exceeds 95%, excess requests are dropped entirely.

Scale-down is even more conservative. HPA uses a stabilization window — it looks at the highest desired replica count over the entire window period before scaling down. This prevents the autoscaler from prematurely removing pods after a brief traffic dip, only to need them again seconds later.

Decisions that matter

Target CPU % (10–90%): The utilization HPA aims to maintain. Setting this to 50% means HPA tries to keep every pod at half capacity — leaving room for traffic spikes. At 80%, you’re running leaner but one spike could push pods past the drop threshold before new replicas come online.

Scale-Up Cooldown (15–300 seconds): Minimum time between consecutive scale-up decisions. At 15s, HPA reacts fast but risks oscillation if traffic is bursty. At 300s, you’re protected from thrashing but severely delayed during genuine ramp-ups. Production sweet spot is typically 60–90 seconds.

Scale-Down Cooldown / Stabilization Window (60–600 seconds): How long HPA waits before removing pods after load drops. The default in Kubernetes is 300 seconds. Aggressive values like 60s save money faster but risk removing capacity you’ll need again in 2 minutes.

Pod Startup Time (5–60 seconds): How long new pods take to become Ready. This is a function of your container image size, init containers, JVM warmup, database connection establishment, and readiness probe configuration. Every second here is a second your existing pods are absorbing the overload.

Min/Max Replicas (1–5 / 5–50): The floor and ceiling. minReplicas should cover your baseline traffic plus some headroom. maxReplicas caps your cloud spend — at 0.05/pod/hour,50podsrunningfor24hourscosts0.05/pod/hour, 50 pods running for 24 hours costs 60/day.

Where it breaks: Scaling Flapping (Thrashing)

The failure mode: Your scale-up cooldown is 15 seconds and scale-down cooldown is 60 seconds. Traffic oscillates between 400 and 600 rps every 45 seconds. HPA scales from 4 to 6 pods, then traffic dips, and 70 seconds later it scales back to 4.

Traffic rises again, and HPA scales back to 6. Pods are constantly starting and terminating.

What you will notice: In Prometheus, you’ll see kube_horizontalpodautoscaler_status_desired_replicas oscillating rapidly. Pod churn shows up as high kube_pod_container_status_restarts_total.

Grafana dashboards will show a sawtooth pattern in replica count. Your cluster node autoscaler may also thrash, adding and removing nodes every few minutes — visible in kube_node_status_condition.

Hint The interactive demo’s “Scaling Flapping” preset demonstrates this scenario. Try setting the scale-up cooldown to 15s and scale-down cooldown to 60s to reproduce the sawtooth pattern.

What I would do:

# Increase stabilization window to prevent premature scale-down
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Pods
          value: 4
          periodSeconds: 60

Set the scale-down stabilizationWindowSeconds to at least 300 seconds. Use percentage-based scale-down policies (remove at most 10% of pods per minute) to prevent cliff-edge capacity drops. Keep scale-up aggressive but rate-limited.

Where it breaks: Database Connection Exhaustion During Scale-Up

The failure mode: HPA scales from 5 to 20 pods in response to a traffic spike. Each pod opens 10 database connections in its connection pool during startup. Your PostgreSQL instance has max_connections = 100.

The 15 new pods try to open 150 connections simultaneously — 50 of them fail. Pods crash-loop with FATAL: too many connections for role "app" errors.

What you will notice: Pods show CrashLoopBackOff status in kubectl get pods. Application logs show connection refused or too-many-connections errors.

The pg_stat_activity view in PostgreSQL shows connections at the limit. Ironically, the crash-loops cause HPA to see fewer Ready pods and even higher CPU, triggering more scale-up attempts that also fail.

Warning Always pre-calculate your max connections budget before setting maxReplicas. Formula: max_connections >= maxReplicas × connectionsPerPod + overhead.

What I would do: Deploy PgBouncer or a similar connection pooler as a sidecar or separate service. Set each pod’s pool to 2–3 connections max, and let PgBouncer multiplex them to PostgreSQL.

# Pod spec with PgBouncer sidecar
containers:
  - name: app
    env:
      - name: DATABASE_URL
        value: "postgresql://localhost:6432/mydb"  # PgBouncer
      - name: DB_POOL_SIZE
        value: "3"
  - name: pgbouncer
    image: pgbouncer/pgbouncer:1.21.0
    ports:
      - containerPort: 6432

Where it breaks: Cold Start Delays Causing Request Drops

The failure mode: Traffic spikes 5× from 200 to 1,000 rps. HPA immediately calculates it needs 10 pods (up from 2). But pod startup time is 45 seconds — the application pulls a 1.2GB container image, runs Flyway migrations, and warms its JIT cache.

During those 45 seconds, the 2 existing pods are handling 500 rps each at 100% CPU. At 95%+ CPU, requests are dropped. You lose approximately 2,000 requests before the new pods are Ready.

What you will notice: Prometheus shows container_cpu_usage_seconds_total pegged at limits. The kube_pod_status_phase{phase="Pending"} metric shows pods stuck in Pending.

Application-level metrics show elevated 5xx rates and p99 latency spiking from 50ms to 8,000ms+ (timeout). Client-side, users see loading spinners and timeout errors.

What I would do: Use warm pools — maintain pre-initialized pods at a low resource profile that can be activated quickly. In Kubernetes, this means using a combination of minReplicas set higher than your strict baseline and PodDisruptionBudgets to prevent scaling below a safe threshold:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: app-pdb
spec:
  minAvailable: 3
  selector:
    matchLabels:
      app: web-frontend

Additionally, optimize your container startup: use distroless base images, pre-pull images to nodes with DaemonSet warmers, move database migrations out of pod startup, and configure aggressive readiness probes (check every 2 seconds instead of 10).

Hint Try the interactive demo’s “Cold Start” preset to see request drops during pod initialization. Experiment with higher minReplicas to reduce the cold start penalty.

The tradeoff

ApproachProsCons
Low target CPU (30%)Headroom absorbs spikes, fewer drops2–3× higher pod count at baseline, ~0.15/hourinsteadof0.15/hour instead of 0.05/hour
High target CPU (80%)Cost efficient at steady stateNo burst capacity, cold starts cause drops during spikes
Aggressive scale-up cooldown (15s)Fast reaction to genuine traffic rampsFlapping risk with bursty traffic, node autoscaler churn
Conservative scale-down cooldown (300s+)Stable capacity, absorbs secondary spikesPods idle for 5+ minutes after traffic drops, wasted cost
Warm pool (high minReplicas)Near-zero cold start penaltyPaying for idle capacity 24/7, $0.05/pod/hour adds up
Connection pooling (PgBouncer)Scales pods without exhausting DBAdded operational complexity, extra hop latency (~1ms)

A practical checklist

Step 1: Identify whether you’re scaling too fast or too slow. Check kubectl get hpa for the current replica count vs. desired. If desired constantly overshoots actual, your cooldowns are too aggressive. If desired lags behind actual load, your target CPU is too high.

Step 2: Examine pod startup time. Run kubectl describe pod <pod-name> and look at event timestamps: Scheduled, Pulling, Pulled, Created, Started, and the first readiness probe success. The gap between Scheduled and Ready is your cold start penalty.

Step 3: Check for resource exhaustion. Use kubectl top pods to see current CPU and memory. Cross-reference with kubectl get events --field-selector reason=OOMKilled for memory leak crashes. Look at pg_stat_activity or your connection pooler metrics for database connection saturation.

Step 4: Tune incrementally. Start with Kubernetes defaults (300s scale-down stabilization, 0s scale-up stabilization). Adjust one knob at a time. Use the interactive demo to test your configuration against traffic patterns before applying to production.

Step 5: Set up alerting. Configure Prometheus alerts for kube_horizontalpodautoscaler_status_desired_replicas != kube_horizontalpodautoscaler_status_current_replicas lasting more than 5 minutes — this catches stuck scale-ups.

Screenshot of the TechViz Hub auto-scaling simulator showing pod replicas scaling under a traffic spike

Try it yourself

Reading about HPA formulas and cooldown windows only gets you so far. The real intuition comes from watching pods spin up and down in real time — seeing the gap between desired and actual replicas, watching requests drop during cold starts, and feeling the flapping when cooldowns are too aggressive.

Try the interactive Auto-Scaling demo to experiment with Black Friday traffic spikes, crash recovery, memory leaks, and every HPA knob — all without risking your production cluster.