Skip to content
Codeloom
CI/CD

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.

·6 min read · By Codeloom
Advanced 11 min read

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:

  1. Deploy a new version alongside or in place of the old version.
  2. Observe health metrics during a bake period.
  3. Decide whether metrics are within acceptable thresholds.
  4. 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.