Automated Deployment Rollbacks with Observability
Build automated rollback systems that use health checks, error rate thresholds, and observability signals to detect failures and revert deployments without human intervention.
What you'll learn
- ✓How to design automated rollback triggers based on observability signals
- ✓Implementing health check gates in deployment pipelines
- ✓Using error rate and latency thresholds for automatic revert decisions
- ✓Building rollback automation with GitHub Actions and Kubernetes
- ✓Progressive delivery with automatic promotion or rollback
Prerequisites
None — this post is self-contained.
Manual rollbacks are slow. When a bad deployment reaches production at 3 AM, the time between detecting the problem and completing the rollback depends on someone waking up, diagnosing the issue, and running the right commands. Automated rollbacks replace that human loop with observability signals that trigger reverts within minutes. This guide covers how to build rollback automation that monitors deployments and reverts them when health metrics cross defined thresholds.
The Automated Rollback Loop
An automated rollback system has four components:
- Deploy a new version alongside or in place of the old version.
- Observe health metrics during a bake period.
- Decide whether metrics are within acceptable thresholds.
- Act by either promoting the new version or reverting to the previous one.
The key difference from manual rollbacks is that step 3 is a programmatic decision based on pre-defined criteria, not a human judgment call.
Health Check Gates
The simplest form of automated rollback is a health check gate that runs immediately after deployment:
# GitHub Actions deployment with health check gate
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
kubectl set image deployment/api \
api=registry.example.com/api:${{ github.sha }}
kubectl rollout status deployment/api --timeout=300s
- name: Health check gate
run: |
MAX_RETRIES=10
RETRY_INTERVAL=15
ENDPOINT="https://api.example.com/health"
for i in $(seq 1 $MAX_RETRIES); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$ENDPOINT")
if [ "$STATUS" = "200" ]; then
echo "Health check passed on attempt $i"
exit 0
fi
echo "Attempt $i: status $STATUS, retrying in ${RETRY_INTERVAL}s"
sleep $RETRY_INTERVAL
done
echo "Health check failed after $MAX_RETRIES attempts"
exit 1
- name: Rollback on failure
if: failure()
run: |
echo "Rolling back deployment"
kubectl rollout undo deployment/api
kubectl rollout status deployment/api --timeout=300s
This catches crashes and startup failures but misses subtle issues like elevated error rates or degraded latency that only appear under load.
Metric-Based Rollback Triggers
For deeper verification, query your observability platform during the bake period. Here is a script that checks error rate and p99 latency from Prometheus:
#!/bin/bash
# scripts/verify-deployment.sh
PROMETHEUS_URL="http://prometheus:9090"
SERVICE="api"
THRESHOLD_ERROR_RATE=0.05 # 5% error rate
THRESHOLD_P99_MS=500 # 500ms p99 latency
BAKE_MINUTES=10
CHECK_INTERVAL=60
echo "Starting deployment verification for $SERVICE"
echo "Bake period: ${BAKE_MINUTES} minutes"
CHECKS=$((BAKE_MINUTES * 60 / CHECK_INTERVAL))
for i in $(seq 1 $CHECKS); do
# Query error rate over the last 5 minutes
ERROR_RATE=$(curl -s "$PROMETHEUS_URL/api/v1/query" \
--data-urlencode "query=rate(http_requests_total{service=\"$SERVICE\",status=~\"5..\"}[5m]) / rate(http_requests_total{service=\"$SERVICE\"}[5m])" \
| jq -r '.data.result[0].value[1] // "0"')
# Query p99 latency over the last 5 minutes
P99_LATENCY=$(curl -s "$PROMETHEUS_URL/api/v1/query" \
--data-urlencode "query=histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service=\"$SERVICE\"}[5m])) * 1000" \
| jq -r '.data.result[0].value[1] // "0"')
echo "Check $i/$CHECKS: error_rate=$ERROR_RATE, p99_ms=$P99_LATENCY"
# Check thresholds
if (( $(echo "$ERROR_RATE > $THRESHOLD_ERROR_RATE" | bc -l) )); then
echo "FAIL: Error rate $ERROR_RATE exceeds threshold $THRESHOLD_ERROR_RATE"
exit 1
fi
if (( $(echo "$P99_LATENCY > $THRESHOLD_P99_MS" | bc -l) )); then
echo "FAIL: P99 latency ${P99_LATENCY}ms exceeds threshold ${THRESHOLD_P99_MS}ms"
exit 1
fi
sleep $CHECK_INTERVAL
done
echo "PASS: All checks passed during bake period"
exit 0
Integrate this into your deployment pipeline:
- name: Verify deployment metrics
run: ./scripts/verify-deployment.sh
- name: Rollback on metric failure
if: failure()
run: |
kubectl rollout undo deployment/api
# Notify the team
curl -X POST "$SLACK_WEBHOOK" \
-d "{\"text\":\"Auto-rollback triggered for api. Error rate or latency exceeded thresholds.\"}"
Kubernetes-Native Rollback
Kubernetes has built-in rollback capabilities. The kubectl rollout undo command reverts to the previous ReplicaSet, which Kubernetes maintains automatically:
# Undo the last deployment
kubectl rollout undo deployment/api
# Undo to a specific revision
kubectl rollout history deployment/api
kubectl rollout undo deployment/api --to-revision=3
For automated rollback, configure readiness and liveness probes to let Kubernetes detect unhealthy pods during rollout:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
template:
spec:
containers:
- name: api
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
With maxUnavailable: 0, Kubernetes never removes old pods until new pods pass their readiness probes. If new pods consistently fail readiness checks, the rollout stalls and can be automatically undone.
Progressive Delivery with Argo Rollouts
For canary deployments with automatic promotion or rollback, Argo Rollouts extends Kubernetes with analysis-driven decisions:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: api
spec:
strategy:
canary:
steps:
- setWeight: 10
- pause: { duration: 5m }
- analysis:
templates:
- templateName: error-rate-check
- setWeight: 50
- pause: { duration: 5m }
- analysis:
templates:
- templateName: error-rate-check
- setWeight: 100
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: error-rate-check
spec:
metrics:
- name: error-rate
interval: 60s
count: 5
successCondition: result[0] < 0.05
provider:
prometheus:
address: http://prometheus:9090
query: |
rate(http_requests_total{service="api",status=~"5.."}[5m])
/
rate(http_requests_total{service="api"}[5m])
Argo Rollouts sends 10% of traffic to the new version, waits 5 minutes, then runs the analysis template. If the error rate is below 5%, it increases to 50%. If any analysis fails, it automatically rolls back to 0% and marks the rollout as degraded.
Choosing Rollback Thresholds
Setting thresholds too tight causes false rollbacks from normal variance. Setting them too loose lets real problems through. Start with these guidelines:
Error rate: Set the threshold at 2-3x your baseline. If your normal 5xx rate is 0.1%, set the rollback threshold at 0.3%.
Latency: Use the p99 at peak traffic as your baseline, then add 50%. If your normal p99 is 200ms at peak, set the threshold at 300ms.
Bake period: Match it to your traffic patterns. A 5-minute bake during low-traffic hours may not exercise the code paths that matter. Consider longer bake periods for off-peak deployments.
Review and adjust thresholds quarterly based on historical data. Track how many rollbacks were true positives vs false positives.
Key Takeaways
Automated rollbacks replace human reaction time with programmatic decisions. Start with health check gates for crash detection, then add metric-based verification for error rates and latency during a bake period. Use Kubernetes readiness probes for pod-level health and tools like Argo Rollouts for traffic-shifting with analysis-driven promotion. Set thresholds based on your baseline metrics with appropriate margins, and always include notification hooks so the team knows when an automated rollback triggers. The goal is not to eliminate human judgment from incident response, but to handle the obvious cases automatically so humans can focus on the complex ones.
Related articles
- CI/CD GitHub Actions vs Jenkins: CI/CD Compared
Compare GitHub Actions and Jenkins for CI/CD pipelines. Covers setup, configuration, plugins, pricing, and when to choose each tool for your team.
- CI/CD Automated Testing in CI/CD: Unit, Integration, and E2E
Structure automated tests in your CI/CD pipeline — unit, integration, and end-to-end testing strategies, parallel execution, and failure handling.
- CI/CD CI/CD Deployment Strategies: Blue-Green, Canary, Rolling
Learn the most popular CI/CD deployment strategies including blue-green, canary, and rolling deployments with practical examples and configuration.
- CI/CD GitHub Actions: Complete CI/CD Guide for Any Project
Set up GitHub Actions CI/CD from scratch — workflows, jobs, caching, secrets, matrix builds, deployment, and reusable patterns for any language or framework.