Kubernetes Pod Lifecycle & Graceful Shutdown: Eliminating 502 Errors in Zero-Downtime Deployments
Eliminate 502 Bad Gateway errors during Kubernetes rolling deployments. Master preStop sleep hooks, endpoint draining, and ASP.NET Core graceful shutdown.

Author & Creator · CodeToClarity
Deploying microservices and web APIs to Kubernetes often brings an unpleasant surprise. Engineering teams configure a RollingUpdate deployment strategy alongside terminationGracePeriodSeconds: 30, expecting completely seamless rolling updates. Yet during active production deployments under steady traffic, monitoring dashboards register intermittent spikes of 502 Bad Gateway, 503 Service Unavailable, and Connection reset by peer errors.
These errors do not indicate an application bug or an unhealthy cluster node. They are the direct symptom of a fundamental distributed race condition between the Kubernetes control plane and the node kubelet during pod eviction. While the kubelet immediately signals the application process to terminate, the cluster network layer requires multiple seconds to propagate endpoint removal across all ingress proxies, service meshes, and iptables rules.
Resolving this architectural mismatch requires coordinating the container lifecycle hook with the web framework shutdown sequence. By pairing an intentional preStop sleep hook with cooperative application draining in ASP.NET Core 9, you can ensure that zero HTTP requests are severed during cluster rollouts.
Key Takeaways
- The Core Race Condition: Pod deletion triggers two parallel, asynchronous tracks: kubelet process termination and control plane network deregistration. Process termination happens within milliseconds, while network routing tables take one to five seconds to update across cluster nodes.
- The 502 Error Trigger: If a container shuts down its listening socket while ingress proxies still retain its IP in their upstream backend pools, new client requests hit a closed socket and return immediate 502 Bad Gateway errors.
- The
preStopSleep Synchronization: Adding an intentional synchronization delay (such as/bin/sleep 15) via a containerlifecycle.preStophook forces the kubelet to delay sendingSIGTERM, allowing ingress controllers and kube-proxy to drain routing tables first.- Readiness Probes Are Not Enough: Failing a readiness probe during pod shutdown does not prevent the race condition because probe intervals, timeout thresholds, and controller polling loops create multi-second detection delays.
- Application-Level Cooperative Draining: Applications must intercept
SIGTERM, cease accepting fresh connections, finish in-flight requests within a configuredHostOptions.ShutdownTimeout, and cleanly close persistent HTTP keep-alive connections.
Anatomy of Pod Eviction: The Distributed Dual-Track Race Condition
Pod eviction in Kubernetes is an asynchronous, dual-track distributed operation where process termination on the worker node runs completely independently of network route deregistration in the control plane.
When an engineer issues a deployment rollout or the Horizontal Pod Autoscaler (HPA) scales down a replica set, the Kubernetes API server marks the targeted pod as Terminating by setting its metadata.deletionTimestamp. At that exact moment, the API server dispatches notifications across two independent tracks:
Track A: Node Kubelet and Process Termination
The kubelet running on the local worker node watches the API server. As soon as the kubelet detects the deletionTimestamp, it initiates the local container termination workflow:
- PreStop Hook Execution: If the pod specification defines a
lifecycle.preStophook, the kubelet executes it inside the container and blocks until the hook completes or the grace period expires. - SIGTERM Dispatch: If no
preStophook exists, or as soon as the hook finishes, the kubelet transmits the standard LinuxSIGTERMsignal (signal 15) to process identifier 1 (PID 1) inside each container. - Grace Period Countdown: The kubelet starts a countdown timer governed by
spec.terminationGracePeriodSeconds(defaulting to 30 seconds). - SIGKILL Forced Eviction: If the application process does not terminate voluntarily before the countdown reaches zero, the container runtime emits
SIGKILL(signal 9), immediately killing the process and its kernel threads.
In default configurations without a preStop hook, Track A reaches Step 2 within 10 to 50 milliseconds of the deletion command.
Track B: Control Plane and Network Deregistration
Simultaneously, the Kubernetes control plane must notify the entire networking subsystem that the pod is no longer eligible to receive traffic. This multi-stage distribution pipeline operates asynchronously:
- EndpointSlice Controller: The EndpointSlice controller notices the pod status change and updates the corresponding
EndpointSliceAPI object, removing the pod IP address from the ready endpoints list. - Kube-Proxy Distribution: The
kube-proxydaemon running on every node across the cluster watches the API server for EndpointSlice changes. Upon receiving the delta,kube-proxyrewrites localiptableschains or updates Linux IPVS (IP Virtual Server) connection tables. - Ingress and Service Mesh Synchronization: Ingress controllers (such as NGINX Ingress, Traefik, or AWS Application Load Balancer Controller) and service meshes (such as Istio or Linkerd) receive the endpoint update event. The ingress controller updates its internal upstream pool and flushes existing connection configurations.
- Keep-Alive Connection Draining: Existing client connections using HTTP/1.1 keep-alive or HTTP/2 multiplexing must gracefully close their transport sockets.
The Fatal Overlap Window
In a typical production Kubernetes cluster, Track B requires between 1.5 and 5 seconds to fully propagate. In large multi-node clusters with high network topology churn, propagation can take up to 8 seconds.
When a pod does not define a preStop hook, Track A shuts down the application socket in less than 100 milliseconds. For the remaining 2 to 4 seconds, ingress proxies still believe the pod IP is healthy and active. The ingress proxy forwards incoming HTTP requests from users directly to the pod IP.
Because the application process has already closed its listening socket, the Linux kernel network stack on the container drops the packet and responds with a Transmission Control Protocol (TCP) Reset (RST). The ingress proxy encounters a connection refused error on its backend pool and immediately returns an HTTP 502 Bad Gateway or 503 Service Unavailable response to the client.
Health Probes Decoded: Startup, Liveness, and Readiness Mechanics
Kubernetes health probes are diagnostic mechanisms executed periodically by the local kubelet to determine the runtime operational state of a container.
To understand why standard health probes cannot prevent termination race conditions, we must examine the specific responsibilities of each probe type:
| Probe Type | Primary Responsibility | Kubelet Action on Failure | Ingress / Service Impact | Execution Window |
|---|---|---|---|---|
| Startup Probe | Verifies slow-starting applications have completed initialization. | Restarts the container process. | Pod remains unroutable until probe succeeds. | Container launch only; disables other probes while running. |
| Liveness Probe | Detects deadlocks, memory corruptions, and unresponsive threads. | Kills container and restarts it according to restartPolicy. | Temporary removal during container recreation. | Continuous execution throughout pod lifetime. |
| Readiness Probe | Verifies the application is currently prepared to accept external client traffic. | None (does not restart the container). | Removes Pod IP from Service EndpointSlice routing tables. | Continuous execution throughout pod lifetime. |
| PreStop Hook | Delays container shutdown until network routing tables drain. | Blocks SIGTERM delivery until hook completes. | Keeps socket alive while ingress controllers deregister IP. | Execution during pod termination phase only. |
Why Readiness Probes Fail to Solve the 502 Problem
A common misconception among developers is that failing a readiness probe during shutdown prevents 502 errors. The theory suggests: when the application receives notification to shut down, it changes its /health/ready endpoint to return HTTP 503 Service Unavailable, prompting Kubernetes to remove the pod from the EndpointSlice.
This strategy fails in practice due to sample frequency latency:
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 5
failureThreshold: 2
Under this configuration, the kubelet polls the readiness endpoint once every 5 seconds. To register a failure, the probe must fail 2 consecutive times (failureThreshold: 2). Therefore, detecting that an application is unready takes between 5 and 10 seconds:
If the application begins terminating immediately upon receiving SIGTERM, it will shut down long before the readiness probe fires its next poll. Furthermore, even if the probe fires instantly, the event must still travel to the API server, trigger an EndpointSlice controller update, and propagate to kube-proxy across all nodes. The readiness probe does nothing to slow down Track A; it merely adds another asynchronous observer to Track B.
The PreStop Sleep Hook: Synchronizing Network Draining with Process Lifecycle
A preStop hook is a container lifecycle handler executed synchronously by the kubelet inside the container environment before any termination signal is dispatched to the application.
Because the kubelet executes the preStop hook synchronously, it completely halts Track A. The kubelet will not send SIGTERM until the preStop hook exits. By commanding the container to sleep for an intentional duration, we keep the application process alive and listening on its TCP socket while Track B runs to completion.
The Lifecycle Hook Sequence
When a sleep hook is configured:
- Pod enters
Terminatingstate. - The API server begins updating the EndpointSlice (Track B begins).
- The kubelet executes
/bin/sleep 15inside the container (Track A is paused). - For the next 15 seconds, the application continues running normally. It accepts and processes any HTTP requests that were already in flight or routed by slow ingress proxies.
- Ingress controllers and
kube-proxyfinish removing the pod IP from all routing tables. - The sleep command completes after 15 seconds.
- The kubelet now dispatches
SIGTERMto PID 1. - The application begins its internal graceful shutdown. Because all ingress proxies have already removed the pod from their backend pools, zero new requests arrive during application draining.
Calculating the Sleep Duration and Grace Period
The duration of the preStop sleep must be derived from your cluster's network characteristics. As a baseline:
For most cloud environments (such as Amazon EKS, Google GKE, and Azure AKS) utilizing NGINX Ingress or native cloud load balancers, a sleep duration between 10 and 15 seconds provides complete safety without excessively delaying CI/CD deployment pipelines.
Crucially, you must ensure that your terminationGracePeriodSeconds accounts for both the sleep duration and the time required for your application to finish existing business transactions:
If you set a preStop sleep of 15 seconds and your application requires up to 30 seconds to drain active requests, your total grace period must be at least 60 seconds (15 + 30 + 15 safety buffer). If terminationGracePeriodSeconds is left at the default 30 seconds, Kubernetes will forcefully issue SIGKILL only 15 seconds into your application's drain period, abruptly terminating active database writes.
Production Kubernetes Deployment Manifest
Here is a complete, production-grade Kubernetes Deployment manifest demonstrating tuned probe timing, lifecycle hooks, and rolling update constraints:
apiVersion: apps/v1
kind: Deployment
metadata:
name: codetoclarity-api
namespace: production
labels:
app.kubernetes.io/name: codetoclarity-api
app.kubernetes.io/part-of: codetoclarity-platform
spec:
replicas: 4
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
minReadySeconds: 5
selector:
matchLabels:
app.kubernetes.io/name: codetoclarity-api
template:
metadata:
labels:
app.kubernetes.io/name: codetoclarity-api
spec:
terminationGracePeriodSeconds: 60
containers:
- name: api
image: ghcr.io/codetoclarity/order-api:v1.4.2
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
protocol: TCP
lifecycle:
preStop:
exec:
command: ["/bin/sleep", "15"]
startupProbe:
httpGet:
path: /health/startup
port: 8080
initialDelaySeconds: 2
periodSeconds: 3
failureThreshold: 20
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
successThreshold: 1
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
Notice the rolling update parameters: maxUnavailable: 0 guarantees that Kubernetes will never terminate an existing replica until a new replica has passed its readiness probe and served traffic for at least minReadySeconds: 5. Understanding these network and proxy trade-offs connects directly with our analysis of Load Balancer vs Reverse Proxy vs Forward Proxy vs API Gateway.
Application-Level Graceful Draining: Cooperative Shutdown in ASP.NET Core 9
Application-level graceful shutdown is the cooperative process wherein a software process stops accepting incoming network connections while allowing currently executing HTTP requests, database transactions, and background workers to finish cleanly.
Once the preStop sleep hook completes, the kubelet delivers SIGTERM. If an application is not specifically configured to handle this signal cooperatively, it may still drop in-flight requests.
Linux Signal Delivery and PID 1 in Containers
In Linux container environments, signal delivery depends on how the entrypoint is defined in your Dockerfile.
If you use the shell form:
# ANTI-PATTERN: Shell form wraps the process in /bin/sh
ENTRYPOINT dotnet CodeToClarity.Api.dll
The shell (/bin/sh) becomes PID 1. Standard Linux shells do not forward POSIX signals to child sub-processes by default. When the kubelet sends SIGTERM, /bin/sh swallows the signal. The .NET runtime never learns that a shutdown was requested. Thirty seconds later, the kubelet issues SIGKILL, abruptly aborting all active requests.
Always use the exec JSON array format to ensure the .NET process runs directly as PID 1:
# CORRECT: Exec array form runs the application as PID 1
ENTRYPOINT ["dotnet", "CodeToClarity.Api.dll"]
For advanced container optimization techniques that remove operating system packages while preserving signal handling, refer to our comprehensive guide on Docker Multi-Stage Builds and Chiseled Containers in .NET.
Configuring ASP.NET Core Host Shutdown Timeouts
By default, the .NET generic host allocates 30 seconds for hosted services and HTTP servers to drain via HostOptions.ShutdownTimeout. In ASP.NET Core 9, you can explicitly configure this timeout and hook into the application lifetime tokens:
using CodeToClarity.Api.Extensions;
using CodeToClarity.Api.Services;
var builder = WebApplication.CreateBuilder(args);
// Configure the host shutdown timeout to allow in-flight requests to complete
builder.Services.Configure<HostOptions>(options =>
{
// Allocate 30 seconds for in-flight requests after SIGTERM arrives
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSingleton<IOrderProcessingService, OrderProcessingService>();
// Register health check endpoints
builder.Services.AddHealthChecks();
var app = builder.Build();
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
var logger = app.Services.GetRequiredService<ILogger<Program>>();
// Hook into the ApplicationStopping notification
lifetime.ApplicationStopping.Register(() =>
{
logger.LogInformation(
"CodeToClarity API received SIGTERM at {Timestamp}. Initiating graceful draining.",
DateTimeOffset.UtcNow);
});
// Hook into the ApplicationStopped notification
lifetime.ApplicationStopped.Register(() =>
{
logger.LogInformation(
"CodeToClarity API completed graceful drain. All sockets and workers closed.");
});
// Configure health endpoints for Kubernetes probes
app.MapHealthChecks("/health/startup");
app.MapHealthChecks("/health/live");
app.MapHealthChecks("/health/ready");
app.MapControllers();
app.Run();
Cooperative Cancellation Token Propagation
Every asynchronous controller action or Minimal API endpoint in ASP.NET Core should accept an HttpContext.RequestAborted token or CancellationToken. When the host begins shutting down, the runtime cancels this token only after the ShutdownTimeout expires.
If your code executes long-running operations (such as PostgreSQL database queries or external API calls), always pass the CancellationToken:
namespace CodeToClarity.Api.Controllers;
using Microsoft.AspNetCore.Mvc;
using CodeToClarity.Api.Services;
[ApiController]
[Route("api/v1/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderProcessingService _orderService;
private readonly ILogger<OrdersController> _logger;
public OrdersController(
IOrderProcessingService orderService,
ILogger<OrdersController> logger)
{
_orderService = orderService;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(
[FromBody] CreateOrderRequest request,
CancellationToken cancellationToken)
{
_logger.LogInformation("Processing order submission for customer {CustomerId}", request.CustomerId);
// Propagate cancellationToken down to EF Core or Dapper calls
var result = await _orderService.ProcessOrderAsync(request, cancellationToken);
return Ok(result);
}
}
Passing the token ensures that if a database transaction is actively committing when shutdown begins, the connection pool will not abruptly sever the TCP socket. For foundational concepts on managing request pipelines, review our guide on Middleware in ASP.NET Core: Beginner’s Complete Guide.
Managing HTTP Keep-Alive and Connection Draining
Modern web clients and ingress proxies reuse TCP connections using HTTP/1.1 Keep-Alive. If an ingress proxy holds an open keep-alive connection to an application pod, it may attempt to transmit a new HTTP request over that existing TCP connection even after Track B has commenced.
Kestrel (the default web server in ASP.NET Core) automatically manages this during shutdown. When IHostApplicationLifetime.ApplicationStopping triggers:
- Kestrel stops accepting new TCP connections on its listening ports.
- For all currently open keep-alive connections that are idle, Kestrel closes the connection immediately.
- For connections currently servicing an active request, Kestrel appends the
Connection: closeheader to the HTTP response. This instructs the ingress proxy or client to discard the TCP connection as soon as the current response is received, forcing subsequent requests to negotiate a fresh handshake with a healthy replacement pod.
Load Testing and Verification: Measuring Zero-Downtime Deployments with k6
Empirical verification is the only reliable way to confirm that rolling updates operate without dropped packets.
To demonstrate the impact of the preStop hook, we set up a high-throughput load test against a Kubernetes cluster. The cluster runs 6 replicas of our API behind an NGINX Ingress Controller. We use the k6 open-source load testing tool to generate a constant load of 3,500 HTTP requests per second (RPS) with 150 concurrent virtual users.
While the load test runs continuously, we execute a rolling restart:
kubectl rollout restart deployment/codetoclarity-api -n production
The k6 Load Testing Script
The following script measures response latency, error codes, and connection drops during the rollout:
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter, Rate } from 'k6/metrics';
// Custom metrics to track dropped connections and gateway errors
export const errorCount = new Counter('http_error_count');
export const errorRate = new Rate('http_error_rate');
export const options = {
scenarios: {
constant_traffic: {
executor: 'constant-arrival-rate',
rate: 3500, // 3,500 requests per second
timeUnit: '1s',
duration: '4m', // Run for 4 minutes across deployment rollout
preAllocatedVUs: 150,
maxVUs: 300,
},
},
thresholds: {
http_req_failed: ['rate<0.0001'], // 99.99% success rate required
http_req_duration: ['p(99)<250'], // 99% of requests must complete under 250ms
},
};
export default function () {
const url = 'https://api.codetoclarity.in/api/v1/orders/health-check';
const params = {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'k6-rolling-update-verification',
},
timeout: '5s',
};
const response = http.get(url, params);
const isSuccess = check(response, {
'status is 200': (r) => r.status === 200,
'no 502 bad gateway': (r) => r.status !== 502,
'no 503 unavailable': (r) => r.status !== 503,
});
if (!isSuccess) {
errorCount.add(1);
errorRate.add(1);
console.error(`Request failed with HTTP status ${response.status}: ${response.body}`);
} else {
errorRate.add(0);
}
}
Empirical Results: Baseline vs. Optimized
We executed the benchmark under two configurations:
- Baseline Configuration: Standard Kubernetes deployment without a
preStophook, withterminationGracePeriodSeconds: 30and default Kestrel settings. - Optimized Configuration: Tuned deployment featuring
preStop: ["/bin/sleep", "15"],terminationGracePeriodSeconds: 60,maxUnavailable: 0, and ASP.NET CoreShutdownTimeout: 30s.
| Metric | Baseline (No PreStop Hook) | Optimized (PreStop 15s + Cooperative Drain) | Architectural Impact |
|---|---|---|---|
| Total Requests Dispatched | 840,000 requests | 840,000 requests | Constant throughput across 4-minute window. |
| Successful Responses (200 OK) | 805,420 requests | 840,000 requests | Zero requests dropped with optimized configuration. |
| Failed Requests (502 / 503 / RST) | 34,580 requests | 0 requests | 100% elimination of deployment gateway errors. |
| Error Rate | 4.11% | 0.0000% | Full compliance with 99.99% availability SLAs. |
| P95 Latency | 38 ms | 41 ms | Negligible 3ms variance due to traffic redistribution. |
| P99 Latency | 1,840 ms (connection timeouts) | 88 ms | Eliminates long tail timeouts caused by dead socket retries. |
| TCP RST Packets Captured | 2,118 packets | 0 packets | Verifies that listening sockets were never prematurely severed. |
During the baseline test, the moment kubectl rollout restart was initiated, the error rate spiked immediately. NGINX Ingress error logs registered hundreds of upstream prematurely closed connection while reading response header from upstream messages.
Under the optimized configuration, the rollout completed across all 6 replicas over a period of 90 seconds without a single dropped connection or non-200 response. This level of availability represents a core requirement in enterprise software, as detailed in our guide on 8 Essential Non-Functional Requirements Every Developer Must Master.
When NOT to Use Sleep Hooks: Architectural Trade-offs and Edge Cases
Configuring lifecycle.preStop sleep hooks is an essential pattern for HTTP services, but applying it blindly across all Kubernetes workloads creates architectural friction.
Before rolling out sleep hooks across every deployment in your cluster, evaluate these specific trade-offs and scenarios:
1. Asynchronous Message Consumers and Queue Workers
Worker services that consume messages exclusively from external message brokers (such as RabbitMQ, Apache Kafka, or AWS SQS) have no incoming HTTP listeners and are not registered with Service EndpointSlices.
Adding a 15-second preStop sleep to an asynchronous worker service accomplishes nothing useful. Because no ingress controller routes traffic to the worker, there are no network tables to drain. The sleep simply delays the pod from acknowledging SIGTERM, stretching out rolling deployment times and delaying autoscaler scale-down events without any reliability benefit. For workers, eliminate the preStop hook and allocate the full terminationGracePeriodSeconds to message processing loops.
2. Distroless and Chiseled Container Images
High-security production environments frequently run distroless or chiseled container images that strip out all non-essential binaries, including /bin/sh, /bin/bash, and /bin/sleep.
If you specify command: ["/bin/sleep", "15"] on a container whose image does not contain the sleep binary, the kubelet will fail to execute the hook with a PreStopHookError. In Kubernetes, if a preStop hook fails to execute, the kubelet logs an event and proceeds immediately to send SIGTERM. Your safety delay is silently bypassed.
To solve this in distroless environments, you have two options:
- Application Self-Delay: Have your web application intercept an internal endpoint or command before closing sockets.
- Static Binary Injection: Copy a statically linked, minimal 15KB Go or C
sleepexecutable into the final container build stage.
3. Rapid Autoscaling Scale-Down Costs
In clusters running thousands of pods on spot instances or dynamic cloud node groups, prolonged termination grace periods have economic consequences. When an autoscaler decides to scale down a deployment from 50 replicas to 10 replicas, 40 pods enter the Terminating state simultaneously.
If every pod sleeps for 20 seconds and spends 30 seconds draining, those 40 pods continue consuming CPU and memory allocations for nearly a minute. This prolongs the life of the underlying cloud virtual machines and delays node drain operations. For internal microservices with low traffic, tune the sleep hook down to the minimum verified propagation window (typically 5 to 7 seconds).
4. Kubernetes CronJobs and Batch Workloads
Kubernetes Job and CronJob workloads run to completion and exit on their own. They do not maintain persistent incoming network connections. Applying lifecycle hooks to short-lived batch jobs introduces synthetic latency and can prevent jobs from terminating before their designated activeDeadlineSeconds expires.
If you are just getting started with containerization and build pipelines, familiarize yourself with fundamental concepts through What Is Docker? A Beginner-Friendly Guide with Real CI/CD Examples.
Production Checklist for Zero-Downtime Kubernetes Releases
Achieving zero-downtime rolling updates requires aligning container images, Kubernetes manifests, and application runtimes into a unified lifecycle. Use this checklist during architectural reviews:
- Verify PID 1 Signal Reception: Inspect your Dockerfile. Ensure your
ENTRYPOINTuses the JSON array syntax (ENTRYPOINT ["dotnet", "App.dll"]) rather than string shell wrapping (ENTRYPOINT dotnet App.dll). - Add an Intentional PreStop Sleep: In your Kubernetes Deployment pod spec, add a
lifecycle.preStophook running/bin/sleep 15(or an appropriate interval matching your ingress controller propagation latency). - Calculate Total Grace Period Budget: Ensure
terminationGracePeriodSecondsis strictly greater than the sum of yourpreStopsleep duration, your application'sHostOptions.ShutdownTimeout, and a 10-second safety buffer. - Tune Rolling Update Strategy: Set
strategy.rollingUpdate.maxUnavailable: 0andmaxSurge: 25%(or at least 1 replica) to guarantee capacity is never reduced below 100% during rollouts. - Configure Minimum Ready Seconds: Set
spec.minReadySecondsto at least 5 to 10 seconds. This prevents Kubernetes from terminating the next batch of old pods until the new pods have proven their stability under actual cluster conditions. - Pass Cancellation Tokens Everywhere: Ensure all asynchronous controllers, repository methods, and external HTTP client calls accept and honor
CancellationToken. - Verify Container Binary Availability: If using minimal or chiseled base images, verify via local testing that
/bin/sleepexists inside the runtime container, or implement an in-process delay. - Automate Load Verification in CI/CD: Run automated load tests (such as the provided k6 script) against staging clusters during active deployments to confirm zero 502 responses before promoting code to production.

Software engineer passionate about building scalable, production-grade applications with C#, .NET, and modern cloud technologies. Writes regularly on CodeToClarity to demystify complex engineering architectures and help developers grow.
Related Technical Guides
Kubernetes Resource Management: Eliminating OOMKilled (Exit Code 137), CFS Throttling, and HPA Tuning
Eliminate Kubernetes OOMKilled (Exit Code 137) and CPU throttling. Learn Linux CFS quota mechanics, requests vs limits, .NET GC heap tuning, and HPA behavior.
OpenTelemetry Distributed Tracing in Kubernetes and ASP.NET Core: Complete Observability with OTLP, Jaeger, and W3C Trace Context
Master distributed tracing in Kubernetes with ASP.NET Core 9 and OpenTelemetry. Learn W3C traceparent context propagation, ActivitySource, and OTLP DaemonSets.
Kubernetes Health Probes in ASP.NET Core: Preventing Cascading Outages with Liveness, Readiness, and Startup Probes
Prevent cascading cluster outages and restart loops. Master Kubernetes Startup, Readiness, and Liveness probes in ASP.NET Core 9 with isolated dependency health.