Zero-Downtime Deployments: The Myth vs Reality
We tested 5 popular deployment strategies under real production load. The results might change how you ship code.
“Zero downtime” is the promise every DevOps tool makes. It’s on every landing page, in every conference talk, embedded in every CI/CD pipeline tutorial. But after deploying to production 2,847 times last year, we’ve learned that “zero” is more of a marketing term than a technical reality.
This isn’t a takedown piece. These strategies work. But they work differently than the docs suggest, and the gap between theory and practice cost us more debugging hours than we’d like to admit.
The Experiment
We run a B2B SaaS platform with approximately 12,000 concurrent users during peak hours. Our stack:
Over six months, we systematically tested five deployment strategies, measuring three things:
- Error Rate During Deploy — HTTP 5xx responses during the deployment window
- Latency Impact — P99 latency change during deployment
- True Deployment Time — From git push to fully stable (not just “rollout complete”)
Here’s what we found.
Strategy 1: Rolling Updates
The Kubernetes default. Old pods are gradually replaced with new ones. Simple, built-in, and the strategy most teams use without thinking about it.
spec:
replicas: 6
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2
maxUnavailable: 1
Our Results
The 0.3% error rate doesn’t sound bad until you realize that’s roughly 10 errors per second during the 4-minute window. For our users, that’s failed form submissions, lost data, and support tickets.
The Hidden Problem
Kubernetes considers a pod “ready” when the readiness probe passes. But Node.js apps often pass the probe before the JIT compiler has warmed up. First requests to new pods were 3-4x slower than normal.
The fix: We added a startup delay and pre-warming script that makes synthetic requests before the pod accepts real traffic.
#!/bin/bash
# Hit critical endpoints to warm up V8
for i in {1..100}; do
curl -s http://localhost:3000/api/health > /dev/null
curl -s http://localhost:3000/api/users/warm > /dev/null
curl -s http://localhost:3000/api/products/warm > /dev/null
done
echo "Prewarm complete"
Strategy 2: Blue-Green Deployment
Run two identical environments. Deploy to the inactive one, test it, then switch traffic. The classic “instant rollback” strategy.
┌─────────────┐
│ Users │
└──────┬──────┘
│
▼
┌─────────────┐
│ Load Balancer│
└──────┬──────┘
│
┌────┴────┐
▼ ▼
┌───────┐ ┌───────┐
│ BLUE │ │ GREEN │
│ v2.1 │ │ v2.2 │
│(live) │ │(idle) │
└───────┘ └───────┘
Our Results
The asterisk on “Yes” is important. Blue-green works beautifully for stateless services. But we hit problems:
- 1 Database migrations — If v2.2 needs a schema change, you can’t just switch back to v2.1
- 2 WebSocket connections — Existing connections don’t magically move to the new environment
- 3 Cost — Running 2x infrastructure adds up ($4,200/month in our case)
Our Approach
We use blue-green only for our API gateway and stateless services. Database-touching services use a different strategy (see #4).
Strategy 3: Canary Releases
Route a small percentage of traffic to the new version. Monitor. Gradually increase. This is the strategy everyone says they want to implement “someday.”
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-canary
port:
number: 80
Our Results
Canary gave us the best error rates, but the deployment time killed our velocity. With 8-12 deploys per day, spending 30+ minutes per deploy wasn’t sustainable.
We also learned that canary requires serious observability investment:
Without automation, someone has to babysit every deploy. We built the automation eventually, but it took two engineers three weeks.
Strategy 4: Feature Flags + Rolling
Decouple deployment from release. Ship the code with features disabled, then enable them gradually via flags. This changed everything for us.
const LaunchDarkly = require('launchdarkly-node-server-sdk');
async function getSearchResults(query, userId) {
const useNewAlgorithm = await ldClient.variation(
'new-search-algorithm',
{ key: userId },
false // default: use old algorithm
);
if (useNewAlgorithm) {
return newSearchService.search(query);
}
return legacySearchService.search(query);
}
Our Results
The key insight: deployments become boring. You’re just shipping code that doesn’t do anything new yet. The risky part—enabling the feature—happens separately, and you control exactly when and for whom.
Strategy 5: Shadow/Dark Launching
Mirror production traffic to the new version without returning its responses to users. Compare results. Only switch when confident. This is the nuclear option.
┌─────────────┐
│ Request │
└──────┬──────┘
│
▼
┌─────────────┐
│ Router │
└──────┬──────┘
│
┌────────┼────────┐
│ │ │
▼ │ ▼
┌───────┐ │ ┌───────┐
│ v2.1 │ │ │ v2.2 │
│ LIVE │ │ │SHADOW │
└───┬───┘ │ └───┬───┘
│ │ │
▼ │ ▼
Response ◄───┘ Logged &
to User Compared
Our Results
Technically perfect. Practically… we use this maybe twice a year for major rewrites. The infrastructure overhead and analysis time make it impractical for daily deploys.
Watch Out For
Shadow traffic can double your database load and third-party API calls. We accidentally burned through a month of Stripe API quota in two days during a shadow test.
What We Actually Use
After six months of testing, here’s our current deployment matrix:
The honest answer is: there’s no single “best” strategy. The right choice depends on your service, your risk tolerance, and how fast you need to ship.
Key Takeaways
“Zero Downtime” Is a Spectrum
0.01% errors is not zero. For high-traffic apps, even tiny percentages mean real user impact. Define what “acceptable” means for your business.
Warm-Up Time Is Real
JIT compilers, connection pools, caches—new instances are always slower than warmed ones. Account for this in your readiness checks.
Deployment ≠ Release
Feature flags let you separate “code is running” from “feature is live.” This single change eliminated 80% of our deployment anxiety.
Observability Is Non-Negotiable
Every advanced strategy requires knowing what’s happening. Invest in per-version metrics before investing in fancy deployment tooling.
Want the Raw Data?
We’ve published our full dataset including error logs, latency percentiles, and deployment timestamps. Check our GitHub repo for the complete analysis.