DevOps• 5 MIN READ
Kirito

Kirito

6/10/20265 min read

The Kubernetes Rollout That Almost Ruined Black Friday

A simple kubectl rollout restart on Black Friday caused a 47-minute outage and $340,000 in lost sales — all because of a missing readinessProbe.

The Kubernetes Rollout That Almost Ruined Black Friday

Black Friday. 6:00 AM. I was on call.

Our e-commerce platform was doing $120,000 per hour. Everything was smooth.

Then I got a Slack message: "Can you update the coupon code service? New promo starts at 7 AM."

"No problem," I thought. "It's just a restart."

The Change

The service was a simple Node.js app running on Kubernetes:

apiVersion: apps/v1 kind: Deployment metadata: name: coupon-service spec: replicas: 5 strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25% template: spec: containers: - name: coupon-service image: coupon-service:latest ports: - containerPort: 3000 # NO readinessProbe # NO livenessProbe

I ran:

kubectl rollout restart deployment/coupon-service

The Cascade Failure

Here's what happened next:

T+0 seconds: Kubernetes started terminating old pods.

T+30 seconds: Old pods terminated. New pods started.

T+45 seconds: New pods were running — but not ready.

The Node.js app needed to:

  1. Connect to Redis
  2. Load coupon data from PostgreSQL
  3. Warm up caches

This took 90 seconds total.

But Kubernetes didn't know that.

T+60 seconds: Kubernetes marked pods as "Running" (container started = ready).

Traffic started flowing to pods that weren't ready.

T+75 seconds: The first request hit an unprepared pod.

The pod crashed.

Kubernetes restarted it.

T+90 seconds: Crash loop began.

T+120 seconds: All 5 pods were crash-looping.

Zero healthy replicas.

The Outage

The coupon service was a dependency for:

  • Cart calculations
  • Checkout flow
  • Price displays

When it went down:

Error: Coupon service unavailable

Every cart, every checkout, every page trying to display a price.

The entire site degraded.

47 minutes later, I figured out what happened and fixed it.

Lost revenue: ~$340,000.

Why It Happened

I didn't understand Kubernetes readiness probes.

A readinessProbe tells Kubernetes when a pod is actually ready to receive traffic.

Without it, Kubernetes assumes the pod is ready as soon as the container starts.

# WHAT I SHOULD HAVE HAD readinessProbe: httpGet: path: /health/ready port: 3000 initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 3 successThreshold: 1

My app had a /health/ready endpoint that returned 200 only when:

  • Database connected
  • Redis connected
  • Caches warmed
  • All dependencies healthy

But I never configured Kubernetes to use it.

The Emergency Fix

While the site was down, I had to:

1. Stop the crash loop

# Scale to zero kubectl scale deployment/coupon-service --replicas=0 # Let everything settle sleep 30 # Scale back up slowly kubectl scale deployment/coupon-service --replicas=1

2. Wait for manual health check

# Monitor pod logs kubectl logs -f deployment/coupon-service # Check if it's actually ready kubectl exec deployment/coupon-service -- curl localhost:3000/health/ready

3. Add readiness probe without downtime

# Edit deployment kubectl edit deployment/coupon-service # Add readinessProbe section # Save and exit - Kubernetes rolls out new pods with probe

The Root Cause Analysis

After the crisis, I investigated why this happened.

Problem 1: No readiness probe → Traffic sent to unprepared pods

Problem 2: No startup probe → Slow-starting apps crash before ready

Problem 3: No podDisruptionBudget → All pods terminated at once

Problem 4: No circuit breakers → Downstream services kept trying

The Correct Configuration

Here's what I implemented afterward:

apiVersion: apps/v1 kind: Deployment metadata: name: coupon-service spec: replicas: 5 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # Only 1 extra pod at a time maxUnavailable: 0 # Never go below desired replicas template: spec: containers: - name: coupon-service image: coupon-service:latest ports: - containerPort: 3000 # STARTUP probe for slow-starting apps startupProbe: httpGet: path: /health/startup port: 3000 initialDelaySeconds: 0 periodSeconds: 5 failureThreshold: 30 # Allow 150 seconds total # READINESS probe for traffic routing readinessProbe: httpGet: path: /health/ready port: 3000 initialDelaySeconds: 0 # Startup probe handles initial delay periodSeconds: 5 failureThreshold: 3 successThreshold: 1 # LIVENESS probe for crash detection livenessProbe: httpGet: path: /health/live port: 3000 initialDelaySeconds: 60 periodSeconds: 30 failureThreshold: 3 resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: coupon-service-pdb spec: minAvailable: 3 selector: matchLabels: app: coupon-service

The Application Changes

I also fixed the app to handle signals properly:

// Graceful shutdown in Node.js process.on('SIGTERM', async () => { console.log('SIGTERM received, starting graceful shutdown'); // Stop accepting new requests server.close(() => { console.log('HTTP server closed'); }); // Close database connections await db.disconnect(); await redis.quit(); console.log('Graceful shutdown complete'); process.exit(0); }); // Health endpoints app.get('/health/startup', (req, res) => { // Returns 200 as soon as app starts res.send('ok'); }); app.get('/health/ready', async (req, res) => { const checks = await Promise.all([ db.ping(), redis.ping(), cacheWarmed() ]); if (checks.every(c => c === true)) { res.send('ready'); } else { res.status(503).send('not ready'); } });

What I Learned

1. Readiness Probes Are Not Optional

Every deployment needs proper probes. Every single one.

2. Test Rollouts in Staging With Real Traffic

# Simulate rollout during load test kubectl rollout restart deployment/coupon-service # Watch for errors kubectl get events --watch

3. Use Progressive Rollouts

# Don't restart all at once kubectl rollout restart deployment/coupon-service --dry-run=client -o yaml | kubectl patch -f - --type='json' -p='[{"op": "replace", "path": "/spec/strategy/rollingUpdate/maxUnavailable", "value": 0}]'

4. Implement PodDisruptionBudget

Always set minimum available replicas during voluntary disruptions.

Commands for Safe Rollouts

Check pod readiness

# Wait for rollout to complete kubectl rollout status deployment/coupon-service --timeout=5m # Check readiness of each pod kubectl get pods -l app=coupon-service -o json | jq '.items[].status.conditions[] | select(.type=="Ready") | .status'

Simulate a rollout

# Dry run kubectl rollout restart deployment/coupon-service --dry-run=client # Watch in real-time kubectl get pods -l app=coupon-service -w

Rollback instantly

# Undo the rollout kubectl rollout undo deployment/coupon-service # Check history kubectl rollout history deployment/coupon-service

Conclusion

That Black Friday cost me weeks of sleepless nights.

The company kept me, but I was written up.

Now I have a pre-rollout checklist:

  • [ ] Readiness probe configured
  • [ ] Liveness probe configured
  • [ ] Startup probe configured (for slow apps)
  • [ ] PodDisruptionBudget exists
  • [ ] RollingUpdate strategy has maxUnavailable=0
  • [ ] Graceful shutdown implemented
  • [ ] Load tested in staging

Kubernetes gives you powerful tools. But missing one YAML field can cost millions.

Never roll out without probes. Never.