“Why are users seeing 502 errors during deployments?” This question triggered a deep investigation into our Kubernetes health check implementation with AWS Application Load Balancer. What we discovered was a complex dance between Kubernetes pod lifecycle, ALB target group health checks, and application shutdown behavior that, when misaligned, creates windows of unavailability during routine operations.

This post details our journey from sporadic 502 errors during every deployment to achieving true zero-downtime updates, including the production-tested patterns, gotchas, and monitoring strategies that make it all work reliably at scale.

The Problem: 502 Errors During Deployments

Initial Symptoms

Our monitoring showed a consistent pattern:

  • Every deployment triggered a spike in 502 Bad Gateway errors
  • Errors lasted 5-15 seconds during pod rollout
  • Some user sessions were abruptly terminated
  • The error rate increased with deployment frequency

A typical deployment looked like this:

kubectl rollout status deployment api-server -n production

Waiting for deployment "api-server" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "api-server" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "api-server" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "api-server" rollout to finish: 1 old replicas are pending termination...
deployment "api-server" successfully rolled out

During the “old replicas pending termination” phase, users experienced errors. Our Prometheus metrics showed:

rate(http_requests_total{status="502"}[1m]) during deployment
# Spike from baseline 0/sec to 45 errors/sec

Root Cause Investigation

We started by examining the timing of events during a deployment:

# Terminal 1: Watch pod status
kubectl get pods -n production -l app=api-server -w

# Terminal 2: Watch ALB target health
aws elbv2 describe-target-health --target-group-arn $TG_ARN --query 'TargetHealthDescriptions[*].[Target.Id,TargetHealth.State]' --output table

The logs revealed a critical timing issue:

T+0s:    New pod created
T+2s:    Pod becomes "Running"
T+2s:    Readiness probe succeeds (immediately)
T+2s:    ALB begins health checking (every 5s)
T+5s:    Old pod receives SIGTERM
T+5s:    Old pod immediately begins shutdown
T+6s:    ALB marks old pod "unhealthy" (first failed check)
T+11s:   ALB marks old pod "draining" (second failed check)
T+11s:   ALB stops sending new connections to old pod
T+305s:  ALB finishes connection draining (300s default)
T+305s:  Old pod deleted

The problem: Between T+5s (SIGTERM) and T+11s (ALB stops routing), the old pod was already shutting down but still receiving traffic, resulting in 502 errors.

Understanding the Components

Kubernetes Pod Lifecycle

When a pod is terminated, Kubernetes follows this sequence:

# Pod Lifecycle During Termination
1. Pod marked for deletion (deletionTimestamp set)
2. Pod removed from endpoints
3. PreStop hook executed (if defined)
4. SIGTERM sent to main container process
5. Grace period timer starts (default: 30s)
6. If still running after grace period, SIGKILL sent
7. Pod removed from API

The key insight: Steps 2 and 3 happen simultaneously, not sequentially.

AWS ALB Health Checks

ALB target group health checks have several configuration parameters:

{
  "HealthCheckEnabled": true,
  "HealthCheckIntervalSeconds": 5,
  "HealthCheckPath": "/health",
  "HealthCheckProtocol": "HTTP",
  "HealthCheckTimeoutSeconds": 2,
  "HealthyThresholdCount": 2,
  "UnhealthyThresholdCount": 2,
  "Matcher": {
    "HttpCode": "200"
  }
}

Critical timing calculation:

Time to mark target unhealthy = HealthCheckIntervalSeconds × UnhealthyThresholdCount
                                = 5s × 2 = 10 seconds minimum

The Timing Gap

The fundamental issue:

Kubernetes endpoint removal:     ~1-2 seconds
ALB health check failure:        ~10 seconds (minimum)
Gap:                             ~8-9 seconds of misrouted traffic

During this gap, ALB thinks the pod is healthy but Kubernetes has already begun shutdown.

Solution Architecture

1. PreStop Hook Implementation

The PreStop hook introduces a delay before the application begins shutdown:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: production
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0  # Critical: Never reduce capacity during rollout
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      terminationGracePeriodSeconds: 60  # Increased from default 30s

      containers:
      - name: api-server
        image: api-server:v2.3.1
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP

        # Lifecycle hooks
        lifecycle:
          preStop:
            exec:
              command:
                - /bin/sh
                - -c
                - |
                  # Wait for ALB to mark target unhealthy and drain connections
                  echo "PreStop: Sleeping for 15 seconds to allow connection draining..."
                  sleep 15

                  # Signal application to stop accepting new connections
                  echo "PreStop: Sending graceful shutdown signal..."
                  kill -TERM 1

                  # Wait for existing requests to complete
                  echo "PreStop: Waiting for existing requests to complete..."
                  sleep 10

        # Health check endpoints
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 2
          successThreshold: 1
          failureThreshold: 2

        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10
          timeoutSeconds: 2
          failureThreshold: 3

        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "1000m"
            memory: "1Gi"

2. Application-Level Graceful Shutdown

The application must handle SIGTERM gracefully:

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync/atomic"
    "syscall"
    "time"
)

type Server struct {
    http.Server
    ready   int32  // Atomic: 0 = not ready, 1 = ready
    healthy int32  // Atomic: 0 = not healthy, 1 = healthy
}

func NewServer(addr string, handler http.Handler) *Server {
    srv := &Server{
        Server: http.Server{
            Addr:         addr,
            Handler:      handler,
            ReadTimeout:  10 * time.Second,
            WriteTimeout: 10 * time.Second,
            IdleTimeout:  120 * time.Second,
        },
    }

    // Add health check endpoints
    mux := http.NewServeMux()
    mux.HandleFunc("/health", srv.healthHandler)
    mux.HandleFunc("/ready", srv.readyHandler)
    mux.Handle("/", handler)

    srv.Handler = mux
    return srv
}

func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
    // Liveness probe - only fails if application is completely broken
    if atomic.LoadInt32(&s.healthy) == 1 {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("healthy"))
    } else {
        w.WriteHeader(http.StatusServiceUnavailable)
        w.Write([]byte("unhealthy"))
    }
}

func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
    // Readiness probe - fails during startup and shutdown
    if atomic.LoadInt32(&s.ready) == 1 {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ready"))
    } else {
        w.WriteHeader(http.StatusServiceUnavailable)
        w.Write([]byte("not ready"))
    }
}

func (s *Server) MarkReady() {
    atomic.StoreInt32(&s.ready, 1)
    atomic.StoreInt32(&s.healthy, 1)
    log.Println("Server marked as ready and healthy")
}

func (s *Server) MarkNotReady() {
    atomic.StoreInt32(&s.ready, 0)
    log.Println("Server marked as not ready (shutting down)")
}

func (s *Server) GracefulShutdown(timeout time.Duration) error {
    // Mark as not ready immediately
    s.MarkNotReady()

    // Allow time for load balancer to detect and stop sending traffic
    log.Printf("Waiting %v for load balancer to drain connections...", timeout)
    time.Sleep(timeout)

    // Create shutdown context
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    // Attempt graceful shutdown
    log.Println("Beginning graceful shutdown...")
    if err := s.Shutdown(ctx); err != nil {
        log.Printf("Graceful shutdown failed: %v", err)
        return err
    }

    log.Println("Graceful shutdown completed")
    return nil
}

func main() {
    // Create server
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Simulate some processing time
        time.Sleep(100 * time.Millisecond)
        fmt.Fprintf(w, "Request processed successfully\n")
    })

    srv := NewServer(":8080", handler)

    // Start server in goroutine
    go func() {
        log.Printf("Server starting on %s", srv.Addr)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server failed: %v", err)
        }
    }()

    // Application initialization (database connections, caches, etc.)
    log.Println("Performing initialization...")
    time.Sleep(2 * time.Second)

    // Mark server as ready to receive traffic
    srv.MarkReady()

    // Wait for interrupt signal
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    sig := <-sigChan
    log.Printf("Received signal: %v", sig)

    // Perform graceful shutdown
    if err := srv.GracefulShutdown(15 * time.Second); err != nil {
        log.Printf("Shutdown error: %v", err)
        os.Exit(1)
    }

    log.Println("Server stopped")
}

3. ALB Target Group Configuration

Optimize ALB target group settings for faster health check response:

apiVersion: v1
kind: Service
metadata:
  name: api-server
  namespace: production
  annotations:
    # AWS Load Balancer Controller annotations
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"

    # Health check configuration
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: "/health"
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: "HTTP"
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-interval: "5"
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-timeout: "2"
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-healthy-threshold: "2"
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-unhealthy-threshold: "2"

    # Connection draining
    service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: |
      deregistration_delay.timeout_seconds=30,
      deregistration_delay.connection_termination.enabled=true

spec:
  type: LoadBalancer
  selector:
    app: api-server
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
    name: http

Terraform configuration for explicit target group management:

resource "aws_lb_target_group" "api_server" {
  name        = "api-server-prod"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "ip"

  health_check {
    enabled             = true
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 2
    interval            = 5
    path                = "/health"
    protocol            = "HTTP"
    matcher             = "200"
  }

  deregistration_delay = 30

  # Connection draining attributes
  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Name        = "api-server-prod"
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

# Target group attributes for connection draining
resource "aws_lb_target_group_attachment" "api_server" {
  for_each = toset(var.pod_ips)

  target_group_arn = aws_lb_target_group.api_server.arn
  target_id        = each.value
  port             = 8080

  lifecycle {
    create_before_destroy = true
  }
}

Advanced Patterns

1. Connection Draining with Metrics

Implement application metrics to monitor active connections during shutdown:

package main

import (
    "context"
    "log"
    "net/http"
    "sync"
    "sync/atomic"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    activeConnections = promauto.NewGauge(prometheus.GaugeOpts{
        Name: "http_active_connections",
        Help: "Number of active HTTP connections",
    })

    requestsInFlight = promauto.NewGauge(prometheus.GaugeOpts{
        Name: "http_requests_in_flight",
        Help: "Number of HTTP requests currently being processed",
    })

    shutdownDuration = promauto.NewHistogram(prometheus.HistogramOpts{
        Name:    "http_shutdown_duration_seconds",
        Help:    "Time taken for graceful shutdown",
        Buckets: []float64{1, 5, 10, 15, 20, 30, 45, 60},
    })
)

type InstrumentedServer struct {
    *http.Server
    activeReqs int64
    wg         sync.WaitGroup
}

func NewInstrumentedServer(addr string) *InstrumentedServer {
    srv := &InstrumentedServer{
        Server: &http.Server{
            Addr: addr,
        },
    }

    mux := http.NewServeMux()

    // Metrics endpoint
    mux.Handle("/metrics", promhttp.Handler())

    // Health endpoints
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("healthy"))
    })

    mux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ready"))
    })

    // Main handler with request tracking
    mux.HandleFunc("/", srv.instrumentedHandler)

    // Connection state tracking
    srv.Server.ConnState = srv.connStateHandler

    srv.Handler = mux
    return srv
}

func (s *InstrumentedServer) instrumentedHandler(w http.ResponseWriter, r *http.Request) {
    // Track request start
    s.wg.Add(1)
    atomic.AddInt64(&s.activeReqs, 1)
    requestsInFlight.Inc()

    defer func() {
        // Track request completion
        atomic.AddInt64(&s.activeReqs, -1)
        requestsInFlight.Dec()
        s.wg.Done()
    }()

    // Process request
    time.Sleep(100 * time.Millisecond) // Simulate work
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func (s *InstrumentedServer) connStateHandler(conn net.Conn, state http.ConnState) {
    switch state {
    case http.StateNew:
        activeConnections.Inc()
    case http.StateClosed, http.StateHijacked:
        activeConnections.Dec()
    }
}

func (s *InstrumentedServer) GracefulShutdown(maxWait time.Duration) error {
    startTime := time.Now()
    defer func() {
        duration := time.Since(startTime).Seconds()
        shutdownDuration.Observe(duration)
        log.Printf("Shutdown completed in %.2f seconds", duration)
    }()

    // Create shutdown context
    ctx, cancel := context.WithTimeout(context.Background(), maxWait)
    defer cancel()

    // Stop accepting new connections
    if err := s.Shutdown(ctx); err != nil {
        log.Printf("Shutdown error: %v", err)
        return err
    }

    // Wait for in-flight requests with monitoring
    done := make(chan struct{})
    go func() {
        ticker := time.NewTicker(1 * time.Second)
        defer ticker.Stop()

        for {
            select {
            case <-done:
                return
            case <-ticker.C:
                active := atomic.LoadInt64(&s.activeReqs)
                if active > 0 {
                    log.Printf("Waiting for %d in-flight requests to complete...", active)
                }
            }
        }
    }()

    // Wait for all requests to complete
    s.wg.Wait()
    close(done)

    log.Println("All requests completed")
    return nil
}

2. Readiness Gate for ALB Target Health

Use Kubernetes readiness gates to coordinate with ALB health status:

apiVersion: v1
kind: Pod
metadata:
  name: api-server
  labels:
    app: api-server
spec:
  readinessGates:
  - conditionType: "target-health.alb.kubernetes.aws/api-server_http"

  containers:
  - name: api-server
    image: api-server:v2.3.1
    ports:
    - containerPort: 8080

    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5

This requires the AWS Load Balancer Controller with target health integration:

# aws-load-balancer-controller configuration
apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-load-balancer-controller-config
  namespace: kube-system
data:
  enable-pod-readiness-gate-inject: "true"

3. HPA Configuration for Auto-scaling

Configure Horizontal Pod Autoscaler to maintain capacity during deployments:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server

  minReplicas: 3
  maxReplicas: 20

  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # Wait 5 minutes before scaling down
      policies:
      - type: Percent
        value: 10  # Scale down by max 10% of current replicas
        periodSeconds: 60
      - type: Pods
        value: 2  # Or scale down by max 2 pods
        periodSeconds: 60
      selectPolicy: Min  # Use the policy that results in fewer pods removed

    scaleUp:
      stabilizationWindowSeconds: 0  # Scale up immediately
      policies:
      - type: Percent
        value: 50  # Scale up by max 50% of current replicas
        periodSeconds: 60
      - type: Pods
        value: 4  # Or scale up by max 4 pods
        periodSeconds: 60
      selectPolicy: Max  # Use the policy that results in more pods added

  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"  # 1000 req/s per pod

4. PodDisruptionBudget for Availability

Ensure minimum availability during voluntary disruptions:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-server-pdb
  namespace: production
spec:
  minAvailable: 2  # Always keep at least 2 pods running
  selector:
    matchLabels:
      app: api-server

Testing and Validation

Load Testing During Deployments

Create a test harness that validates zero-downtime deployments:

#!/bin/bash
# test-zero-downtime-deployment.sh

set -euo pipefail

NAMESPACE="production"
DEPLOYMENT="api-server"
SERVICE_URL="https://api.example.com"
DURATION=300  # 5 minutes
RPS=100  # Requests per second

echo "=== Zero-Downtime Deployment Test ==="
echo "Namespace: $NAMESPACE"
echo "Deployment: $DEPLOYMENT"
echo "Service URL: $SERVICE_URL"
echo "Duration: ${DURATION}s"
echo "Target RPS: $RPS"
echo ""

# Start load generator in background
echo "Starting load generator..."
vegeta attack \
    -rate=$RPS \
    -duration=${DURATION}s \
    -targets=<(echo "GET $SERVICE_URL/health") \
    | tee results.bin \
    | vegeta report

# Trigger deployment after 60 seconds
sleep 60

echo "Triggering deployment..."
kubectl set image deployment/$DEPLOYMENT \
    -n $NAMESPACE \
    api-server=api-server:v2.3.2

# Wait for deployment to complete
kubectl rollout status deployment/$DEPLOYMENT -n $NAMESPACE

# Analyze results
echo ""
echo "=== Load Test Results ==="
vegeta report -type=text results.bin

# Check for any errors
ERROR_COUNT=$(vegeta report -type=json results.bin | jq '.status_codes | to_entries[] | select(.key != "200") | .value' | paste -sd+ | bc)

if [ "$ERROR_COUNT" -gt 0 ]; then
    echo "ERROR: Found $ERROR_COUNT failed requests during deployment"
    exit 1
else
    echo "SUCCESS: Zero errors during deployment"
    exit 0
fi

Monitoring Dashboard

Create a Grafana dashboard to visualize deployment health:

{
  "dashboard": {
    "title": "Deployment Health - API Server",
    "panels": [
      {
        "title": "Request Success Rate",
        "targets": [
          {
            "expr": "sum(rate(http_requests_total{job=\"api-server\",status=~\"2..\"}[1m])) / sum(rate(http_requests_total{job=\"api-server\"}[1m]))"
          }
        ],
        "thresholds": [
          { "value": 0.99, "color": "green" },
          { "value": 0.95, "color": "yellow" },
          { "value": 0, "color": "red" }
        ]
      },
      {
        "title": "Active Connections During Rollout",
        "targets": [
          {
            "expr": "http_active_connections{job=\"api-server\"}"
          }
        ]
      },
      {
        "title": "Pod Status",
        "targets": [
          {
            "expr": "kube_pod_status_phase{namespace=\"production\",pod=~\"api-server-.*\"}"
          }
        ]
      },
      {
        "title": "ALB Target Health",
        "targets": [
          {
            "expr": "aws_alb_target_health{target_group=\"api-server-prod\"}"
          }
        ]
      },
      {
        "title": "Graceful Shutdown Duration",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(http_shutdown_duration_seconds_bucket[5m]))"
          }
        ]
      }
    ],
    "annotations": {
      "list": [
        {
          "name": "Deployments",
          "datasource": "Prometheus",
          "expr": "changes(kube_deployment_status_observed_generation{namespace=\"production\",deployment=\"api-server\"}[1m]) > 0"
        }
      ]
    }
  }
}

Production Checklist

Before deploying to production, validate:

Application Level

  • Graceful shutdown implemented (handles SIGTERM)
  • Separate /health and /ready endpoints
  • /ready returns 503 during shutdown
  • Active connection tracking
  • Request completion monitoring
  • Metrics exported (Prometheus format)

Kubernetes Configuration

  • PreStop hook configured (15-30s delay)
  • terminationGracePeriodSeconds >= 60s
  • readinessProbe configured correctly
  • livenessProbe distinct from readinessProbe
  • maxUnavailable = 0 in deployment strategy
  • PodDisruptionBudget configured
  • HPA with appropriate scale-down policies

AWS ALB Configuration

  • Health check interval optimal (5-10s)
  • Unhealthy threshold = 2
  • Deregistration delay = 30-60s
  • Connection draining enabled
  • Target group attributes validated

Monitoring

  • Deployment annotation in Grafana
  • Alerting for elevated error rates
  • Request duration tracking
  • Connection drain monitoring
  • Load test automation

Lessons Learned

1. Timing is Everything

The timing between Kubernetes, ALB, and application shutdown must be carefully orchestrated. Our production configuration:

T+0s:    SIGTERM received
T+0s:    /ready returns 503 (stop accepting new requests)
T+0-5s:  ALB health check may still show healthy
T+5s:    ALB health check fails
T+10s:   ALB marks target unhealthy (2 failures)
T+10s:   ALB stops routing traffic
T+10-15s: PreStop hook sleep completes
T+15s:   Application begins shutdown
T+15-30s: Existing requests complete
T+30s:   ALB deregistration delay completes
T+30s:   Pod deleted

2. Separate Health and Readiness

Never use the same endpoint for liveness and readiness probes:

/health  - Liveness:  Only fails if application is broken (rarely fails)
/ready   - Readiness: Fails during startup AND shutdown (fails frequently)

3. Monitor Connection Draining

Implement metrics to verify connections are actually draining:

// Track how many requests complete during shutdown
shutdownRequestsCompleted = prometheus.NewCounter(...)

// Track maximum time to drain
shutdownMaxDrainTime = prometheus.NewGauge(...)

4. Test Under Load

Deployments behave differently under load. Always test with realistic traffic:

  • Load testing reveals timing issues
  • Verify error rates remain at 0% during rollouts
  • Test with various request durations (long and short)
  • Validate connection draining with long-lived connections

5. HPA Scale-Down Policies

Configure HPA to avoid scaling down during traffic spikes:

  • Use stabilizationWindow to prevent flapping
  • Set conservative scale-down rates
  • Consider business hours in scaling decisions

Conclusion

Achieving zero-downtime deployments with Kubernetes and AWS ALB requires careful coordination of multiple components:

  1. Application-level graceful shutdown that properly handles SIGTERM
  2. PreStop hooks that introduce appropriate delays
  3. ALB configuration optimized for fast health checks and connection draining
  4. Monitoring and alerting to verify the system works as expected

Since implementing these patterns:

  • 502 errors during deployments reduced from 45/sec to 0/sec
  • Deployment confidence increased across teams
  • Automated deployment frequency increased 3x
  • Mean time to deploy reduced from 30 minutes to 8 minutes

The investment in proper health checks and graceful shutdown pays dividends in system reliability and developer productivity.

Additional Resources

For consultation on zero-downtime deployments and AWS/Kubernetes architecture, contact mmattox@support.tools.