← Blog

Kubernetes · Reliability · Deployments

Why Can a “Successful” Kubernetes Rolling Deployment Still Drop Requests?

A Kubernetes Deployment can report a successful rollout while users still see 502s, 503s, or connection resets. This article explains the gap between Pod availability and request completion, including EndpointSlice termination state, load-balancer propagation, graceful shutdown, preStop hooks, health probes, rollout capacity, long-lived connections, PodDisruptionBudgets, and how to test deployments with real in-flight traffic.

Aashaya Aryal

July 11, 2026 · 23 min read

Traffic backed up across several highway lanes while an adjacent lane remains open.

The deployment finishes successfully:

TEXT
deployment "checkout-api" successfully rolled out

Every new Pod reports:

TEXT
READY 1/1
STATUS Running

The old ReplicaSet scales to zero, Kubernetes reports exactly the state we wanted, and yet production traffic during the rollout contains:

TEXT
502
502
connection reset
503

At first, that feels contradictory. If the rollout succeeded, why did requests fail?

Because Kubernetes answering:

“Did I successfully replace the old Pods with healthy new Pods?”

is not the same as the application answering:

“Did every request continue flowing safely while that replacement happened?”

A rolling deployment coordinates several moving parts:

TEXT
Deployment controller
ReplicaSets
Pods
EndpointSlices
kubelet
Service routing
external load balancer
application process
active TCP connections

Those pieces do not all change state atomically.

When an old Pod begins terminating, Kubernetes starts updating its endpoint state and initiates graceful shutdown. EndpointSlices deliberately retain terminating endpoints for a period of time, and an external load balancer may have its own propagation and connection-draining behavior. Existing TCP connections can survive yet another amount of time.

That creates a dangerous interval:

TEXT
Kubernetes has decided:
"this Pod is leaving"

while

some network component or existing connection
still has work associated with that Pod

If the process disappears before that work drains, the request disappears with it.

So the interesting question is not whether Kubernetes supports rolling deployments. Of course it does.

The more useful question is:

What has to remain true between Pod deletion and the final request leaving that Pod?

RollingUpdate Protects Pod Capacity, Not Request Completion

A Kubernetes Deployment normally uses the RollingUpdate strategy. Two important controls are maxUnavailable and maxSurge:

YAML
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 25%
    maxSurge: 25%

maxUnavailable limits how many desired replicas may be unavailable during the rollout, while maxSurge limits how many additional Pods may temporarily exist above the desired replica count.

With four replicas, a rollout might evolve roughly like this:

TEXT
Old: 4
New: 0

then:

TEXT
Old: 3
New: 2

then:

TEXT
Old: 2
New: 3

and eventually:

TEXT
Old: 0
New: 4

Conceptually:

Diagram

4 × v1

3 × v1

2 × v2

2 × v1

3 × v2

4 × v2

That gives the Deployment controller a capacity envelope for replacing Pods.

What it does not provide is a request-completion guarantee.

Even:

YAML
maxUnavailable: 0

does not mean:

TEXT
zero failed requests

It means Kubernetes is controlling Pod availability according to the rollout strategy.

Whether a request that already reached an old Pod survives termination is a different problem.

Follow One Request Through Pod Termination

Consider three replicas behind a load balancer:

Diagram

Load Balancer

Pod A

Pod B

Pod C

Now Kubernetes decides to terminate Pod A.

At a high level, graceful termination involves several steps:

  1. The Pod receives a deletion timestamp.
  2. Its endpoint becomes terminating.
  3. Normal readiness for the terminating endpoint becomes false.
  4. The kubelet begins the shutdown process.
  5. Lifecycle hooks and termination signals are processed.
  6. The Pod is given its configured grace period.
  7. Remaining processes can eventually be terminated forcibly if they do not exit.

The important detail is that these are not one atomic instruction broadcast to every component involved in routing traffic.

There is propagation.

Diagram
ClientPodLoad BalancerEndpointSliceKubernetesClientPodLoad BalancerEndpointSliceKubernetesRouting state propagatesPod deletion beginsEndpoint terminating / ready=falseNew requestRequest may still be associatedwith old backend / connectionShutdown continues

That creates the central race in this article:

The application can terminate faster than traffic can disappear.

Kubernetes may already understand that the endpoint is leaving while another component still has a stale backend view, an established connection, or work already executing on the Pod.

A safe rollout therefore requires more than replacing Pods in the right order. It requires preserving the old instance long enough for the traffic path to converge.

Endpoint Removal and Load-Balancer Draining Are Different Timelines

Modern Kubernetes EndpointSlices expose useful endpoint conditions including:

TEXT
ready
serving
terminating

A terminating endpoint can temporarily resemble:

JSON
{
  "ready": false,
  "serving": true,
  "terminating": true
}

That distinction is intentional.

It communicates something closer to:

TEXT
Do not select me for ordinary new traffic.

But I am still alive and may be serving
work that has not finished yet.

That is very different from:

TEXT
endpoint removed
→ process dies immediately

The separation gives implementations room to support connection draining.

An external load balancer is another state machine

Inside Kubernetes, EndpointSlice state can change relatively quickly.

A cloud load balancer is another system with another representation of backend health.

A path can look like:

Diagram

User

Cloud Load Balancer

Backend / NEG

Kubernetes Service

Pod

Several components in that diagram may independently maintain some version of:

TEXT
Is this endpoint still available?

Those views can converge quickly without converging instantaneously.

Google documents a concrete version of this problem for GKE Network Endpoint Groups. During workload scale-down, prematurely closing existing connections can result in 502 responses, while traffic continuing toward endpoints that have already disappeared can result in 503 responses.

That is exactly the kind of issue that makes a rollout look healthy from the Deployment controller while user requests still fail.

Existing connections complicate removal

HTTP keep-alive makes the problem more subtle.

Even if a load balancer stops establishing new connections to a Pod, it may still have an existing connection associated with that backend.

No request may be active at the exact moment termination begins, yet another request can arrive over that persistent connection shortly afterward.

Graceful draining may therefore require the application or proxy to:

TEXT
stop accepting new connections
stop accepting new requests on retiring connections
allow active requests to finish
close idle connections deliberately

Stopping a listener and finishing requests are related operations.

They are not exactly the same thing.

Graceful Shutdown Is Application Behavior

Receiving SIGTERM is not an instruction to disappear as quickly as possible.

Suppose an application effectively does this:

JAVA
Runtime.getRuntime()
    .addShutdownHook(
        new Thread(() -> {
            System.exit(0);
        })
    );

Or perhaps the framework receives the termination signal and immediately closes everything.

From the process's point of view, that can look perfectly orderly:

TEXT
received SIGTERM
→ exited

Operationally, it may be disastrous.

Imagine:

TEXT
T+0 ms      Pod termination begins
T+20 ms     application exits
T+300 ms    routing state still propagating
T+500 ms    old connection sends next request

The process is gone.

The connection is not.

A better lifecycle looks like:

TEXT
receive termination
↓
stop accepting new work
↓
keep process alive
↓
finish active requests
↓
close connections
↓
exit

The key word is drain.

preStop can create a propagation window

Kubernetes supports preStop lifecycle hooks.

For example:

YAML
spec:
  terminationGracePeriodSeconds: 60

  containers:
    - name: checkout-api
      image: example/checkout:42

      lifecycle:
        preStop:
          exec:
            command:
              - /bin/sh
              - -c
              - sleep 15

A blunt:

TEXT
sleep 15

is not sophisticated, but it can serve an important purpose:

TEXT
Pod deletion begins
↓
routing systems get time to stop choosing endpoint
↓
process remains alive
↓
SIGTERM / application shutdown proceeds

Cloud load-balancer environments sometimes recommend this kind of delay because endpoint deprogramming takes time. GKE's guidance, for example, discusses coordinating connection draining, preStop, and the overall Pod termination grace period.

The number itself is workload-specific.

Do not copy:

TEXT
sleep 120

because one documentation example or blog post happened to use it.

Measure the behavior of your own traffic path.

Blind sleep is useful, but blind

Suppose endpoint removal normally takes:

TEXT
2 seconds

while the hook always sleeps:

TEXT
20 seconds

You spend roughly 18 seconds waiting for no useful reason during every termination.

Now suppose a regional or load-balancer event occasionally takes:

TEXT
35 seconds

The same 20-second delay is still unsafe.

Application-aware draining is more precise when practical. A simple delay remains popular because it is:

TEXT
simple
predictable
easy to test

Whether that simplicity is good enough depends on the failure model.

The Termination Grace Period Is a Budget

A Pod does not have unlimited time to shut down.

Kubernetes uses a default termination grace period of 30 seconds unless you configure another value. When the grace period is exhausted, remaining container processes can eventually be terminated forcibly.

So:

YAML
terminationGracePeriodSeconds: 60

should be treated as a shutdown budget.

Conceptually:

TEXT
T_grace
>=
T_preStop
+
T_application_shutdown
+
safety margin

If an external load balancer has explicit connection-drain or propagation requirements, those timings need to fit into the lifecycle as well.

Consider:

TEXT
preStop = 20 sec
longest request = 45 sec
termination grace = 30 sec

That configuration contains a mathematical contradiction.

Some valid in-flight request cannot possibly finish before the process loses its grace window.

Long requests force an architectural decision

Imagine:

HTTP
POST /reports/export

can take:

TEXT
45 seconds

A request begins, then Kubernetes starts terminating the Pod two seconds later.

With a 30-second termination budget, something must change.

Possible choices include:

TEXT
increase shutdown time

or:

TEXT
redesign long work as asynchronous

or:

TEXT
support cancellation and resumption

or:

TEXT
make the request safely retryable

A long-running synchronous operation and a short-lived deployment lifecycle are architectural assumptions.

They need to agree.

Health Probes Have Different Jobs

Kubernetes exposes several health concepts that are easy to blur together.

Treating them as interchangeable can turn a normal degradation into an outage.

Readiness asks whether the Pod should receive traffic

A readiness probe answers something like:

Should this instance receive normal production traffic right now?

For example:

YAML
readinessProbe:
  httpGet:
    path: /ready
    port: 8080

  periodSeconds: 5
  failureThreshold: 2

A container can be:

TEXT
alive
but
not ready

and that can be completely valid.

Perhaps it is temporarily unable to accept new work but does not need restarting.

Liveness asks whether restarting would help

A liveness probe asks a much more invasive question:

Is this process unhealthy in a way that makes restart the correct recovery action?

For example:

YAML
livenessProbe:
  httpGet:
    path: /live
    port: 8080

  periodSeconds: 10
  failureThreshold: 3

Repeated liveness failure can cause the container to restart.

Kubernetes explicitly warns that badly designed liveness checks can produce cascading failures. An overloaded container fails its probe, gets restarted, remaining Pods inherit more traffic, those Pods become overloaded too, and capacity keeps shrinking.

Diagram

Traffic Spike

Pod Overloaded

Liveness Probe Times Out

Pod Restarted

Fewer Serving Pods

More Load per Remaining Pod

A mechanism intended to improve resilience has become an outage amplifier.

Do not make liveness depend on every downstream dependency

Suppose /live fails whenever PostgreSQL is temporarily unavailable:

TEXT
PostgreSQL outage
↓
all API Pods fail liveness
↓
Kubernetes restarts entire API fleet

Did restarting the API repair PostgreSQL?

No.

It may actually make recovery harder by producing:

TEXT
connection storms
cache warmup
JVM warmup
new DNS/TLS connections

Now the incident is:

TEXT
database problem
+
application restart problem

Liveness should generally represent whether restarting this container is likely to help.

Readiness can be more sensitive to temporary inability to serve traffic.

Startup probes protect slow initialization

Suppose a Spring service needs:

TEXT
60 seconds

to initialize, warm caches, load configuration, and establish connections.

If liveness begins too early:

TEXT
starting
→ probe fails
→ restart
→ starting
→ probe fails

the application may never finish startup.

A startup probe gives initialization its own window:

YAML
startupProbe:
  httpGet:
    path: /startup
    port: 8080

  failureThreshold: 30
  periodSeconds: 5

That configuration allows up to:

TEXT
30 × 5 = 150 seconds

for startup before Kubernetes treats it as failed.

Each probe has a different semantic job.

Combining them all into one /health endpoint with identical behavior throws those semantics away.

Readiness Must Mean Ready for Production

A new Pod can be technically reachable long before it is safe to send real traffic to it.

Imagine /ready returns:

TEXT
200

the moment the HTTP server binds to port 8080.

The process is alive, but perhaps it has not yet:

TEXT
warmed local caches
initialized expensive clients
loaded reference data
established connection pools
completed JIT warmup

The rollout can now replace warm instances with cold-but-ready instances:

Diagram

terminated

Warm v1

Cold v2

Ready=true

Production Traffic

Gone

The Deployment succeeds.

Latency spikes.

Requests may time out.

Liveness may even begin failing if the cold instance becomes overloaded immediately.

Readiness should therefore mean:

This instance can actually handle production traffic at an acceptable level.

not merely:

The process opened a socket.

Readiness also applies during runtime

Kubernetes continues running readiness probes after startup, so an instance can temporarily leave normal traffic rotation if it becomes unable to serve safely.

That is powerful.

It also requires careful capacity reasoning.

Suppose every Pod marks itself unready when its connection pool becomes heavily utilized:

TEXT
traffic spike
↓
Pod A not ready
↓
Pod B receives more traffic
↓
Pod B not ready
↓
Pod C receives all remaining traffic
↓
Pod C not ready

A protective mechanism can remove the whole fleet from service.

Readiness needs semantics tied to the behavior of the system, not merely a threshold inside one process.

Application-aware drain state

An application can make its own lifecycle explicit:

TEXT
RUNNING
↓
DRAINING
↓
TERMINATED

When draining begins:

TEXT
/ready → 503

/live → 200

existing requests → continue

new expensive work → rejected

A simplified Spring component might track that state:

JAVA
@Component
public class DrainState {

    private final AtomicBoolean draining =
        new AtomicBoolean(false);

    public void startDraining() {
        draining.set(true);
    }

    public boolean isDraining() {
        return draining.get();
    }
}

Readiness can reflect it:

JAVA
@GetMapping("/ready")
public ResponseEntity<Void> ready() {

    if (drainState.isDraining()) {
        return ResponseEntity
            .status(503)
            .build();
    }

    return ResponseEntity.ok().build();
}

Shutdown can then follow:

TEXT
mark draining
→ wait for active requests
→ close dependencies
→ exit

Modern Kubernetes already marks terminating endpoints as not ready for normal Service traffic, so application-aware draining is not simply about manually telling Kubernetes that deletion has started.

Its value is broader:

TEXT
control new application work
expose internal drain state
finish active operations
coordinate external systems

Kubernetes manages endpoint lifecycle.

The application still owns application semantics.

Rollout Capacity Still Needs Headroom

A rollout can be perfectly compliant with its Deployment strategy and still overload the application.

Suppose the service normally runs:

TEXT
4 replicas

and each replica can handle:

TEXT
250 req/s

Total nominal capacity:

TEXT
1,000 req/s

Production traffic:

TEXT
850 req/s

Now imagine the rolling configuration permits one unavailable Pod.

Temporary serving capacity becomes:

TEXT
3 × 250 = 750 req/s

while traffic remains:

TEXT
850 req/s
Diagram

850 req/s

Pod A

250

Pod B

250

Pod C

250

Pod D

Unavailable During Rollout

The rollout is behaving exactly as configured.

The service is overloaded.

maxUnavailable describes allowable Pod unavailability.

It does not know how much spare traffic capacity the workload has.

maxSurge can preserve serving capacity

For a four-replica latency-sensitive service, a strategy such as:

YAML
strategy:
  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1

allows Kubernetes to start another Pod before retiring an old one:

TEXT
4 old
↓
4 old + 1 new
↓
3 old + 2 new
↓
...
↓
4 new

That can preserve nominal HTTP capacity.

It also creates temporary demand elsewhere:

TEXT
extra CPU
extra memory
extra database connections
extra Kafka connections
extra cache connections

Suppose every Pod opens:

TEXT
30 DB connections

Surging several new Pods can put pressure on PostgreSQL even while the HTTP tier looks safer.

Every availability control consumes capacity somewhere.

New Pods can create dependency storms

Imagine ten Pods start together.

Each establishes:

TEXT
50 DB connections

Startup suddenly requests:

TEXT
500 connections

The rollout protected application capacity while hammering the database.

The same pattern can occur with:

TEXT
Redis
Kafka
DNS
TLS handshakes
configuration services

Adding application compute does not automatically add capacity to every shared dependency.

Deployment headroom needs to be evaluated end to end.

Long-Lived and Background Work Need Their Own Drain Semantics

Normal HTTP request/response traffic is only one workload.

A Pod may also own connections or work that lasts minutes or hours.

Long-lived connections

Consider:

TEXT
WebSocket
Server-Sent Events
gRPC streaming

Waiting for every connection to end naturally may make a rollout practically impossible.

A drain policy might instead:

TEXT
stop accepting new streams
send reconnect instruction
allow a short grace period
close remaining streams
let clients reconnect elsewhere

That requires client behavior too.

For some protocols, graceful rollout is not purely a server operation. It is a contract between server and client.

Background workers

Suppose a Pod is consuming Kafka records and currently processing:

TEXT
event 4812

A bad termination path is:

TEXT
SIGTERM
→ exit immediately
→ event partially processed

A safer lifecycle is:

TEXT
receive termination
↓
stop polling for new records
↓
finish current batch
↓
commit appropriate offsets
↓
close consumer
↓
exit

That work still needs to fit inside:

TEXT
terminationGracePeriodSeconds

or the kubelet can eventually terminate the process anyway.

Graceful shutdown is workload-specific.

Sidecars add another shutdown order

A Pod may contain:

TEXT
application
service-mesh proxy
logging agent
security sidecar

Now process ordering matters.

If the network proxy exits before the application finishes its outgoing requests, the application may remain alive but lose the ability to complete its work.

Modern Kubernetes has improved sidecar lifecycle behavior, but the architectural lesson remains:

A Pod can contain several processes whose shutdown order affects correctness.

In environments where injected sidecars participate in request handling, lifecycle behavior needs to account for those containers too.

PodDisruptionBudgets Solve a Different Problem

A PodDisruptionBudget can limit how many Pods are made unavailable by supported voluntary disruption operations.

For example:

YAML
apiVersion: policy/v1
kind: PodDisruptionBudget

metadata:
  name: checkout-api

spec:
  minAvailable: 3

  selector:
    matchLabels:
      app: checkout-api

For a four-replica service, this says that supported voluntary eviction should preserve at least three available replicas.

That is useful.

It is not:

TEXT
a guarantee that Pods cannot disappear

Hardware failure does not consult the PDB first.

Neither do:

TEXT
kernel crash
network loss
OOM
power loss

The difference between voluntary and involuntary failure matters.

PDB is not RollingUpdate configuration

These settings can look similar:

TEXT
Deployment maxUnavailable

and:

TEXT
PodDisruptionBudget

but they apply to different control paths.

A Deployment's rolling-update settings govern how the Deployment controller replaces Pods for the application's own rollout.

A PDB constrains supported voluntary eviction workflows such as node drains.

A production service may need both.

They are not interchangeable.

Graceful Shutdown Cannot Carry Correctness

Everything discussed so far assumes Kubernetes gets to perform an orderly lifecycle.

Real failures also include:

TEXT
node disappears
VM power failure
kernel panic
network partition
hardware failure
process crash

There may be no useful preStop.

No generous drain window.

No friendly SIGTERM.

Graceful shutdown improves planned lifecycle events.

It does not replace correctness mechanisms for abrupt instance loss.

Requests or jobs may still need:

TEXT
idempotency
retries
transactional boundaries
reconciliation

because some processes will eventually disappear without announcing themselves.

Do not put essential state only in shutdown hooks

Suppose critical correctness depends on:

TEXT
SIGTERM
→ flush essential state
→ exit

What happens when the machine loses power?

The hook never runs.

Shutdown hooks are appropriate for:

TEXT
draining
cleanup
closing resources
best-effort finalization

They should not be the only place the system establishes irreversible correctness.

Critical state should already be durable before termination begins.

Test Rollouts With Real In-Flight Traffic

This test:

BASH
kubectl rollout restart deployment/checkout-api
kubectl rollout status deployment/checkout-api

tests whether Kubernetes can complete the rollout.

It does not fully test the application's request lifecycle.

A stronger experiment generates traffic continuously while rolling the service repeatedly.

For example:

TEXT
100 req/s normal requests
+
several 10-second requests
+
persistent keep-alive connections

during:

TEXT
rollout restart

Then measure:

TEXT
HTTP errors
connection resets
p95
p99
in-flight request completion
active connection count
Pod termination timing
EndpointSlice state

Now the test is evaluating the real promise:

TEXT
deployment should not break user requests

instead of merely:

TEXT
deployment eventually becomes Ready

That distinction is the heart of the article.

Watch EndpointSlices during termination

The lifecycle is directly observable:

BASH
kubectl get endpointslices \
  -l kubernetes.io/service-name=checkout-api \
  -o yaml

A terminating endpoint may show:

YAML
conditions:
  ready: false
  serving: true
  terminating: true

while a replacement endpoint reports:

YAML
conditions:
  ready: true
  serving: true
  terminating: false

Kubernetes' graceful-termination documentation demonstrates this transition explicitly.

Seeing it happen in a real rollout makes the traffic handoff much easier to understand than treating Pod lifecycle as an abstract diagram.

Instrument Shutdown as a Production Operation

If graceful shutdown matters to availability, shutdown itself deserves observability.

I would emit lifecycle events such as:

JSON
{
  "event": "shutdown_started",
  "pod": "checkout-7c84d",
  "activeRequests": 17
}

then:

JSON
{
  "event": "readiness_disabled",
  "pod": "checkout-7c84d"
}

then:

JSON
{
  "event": "shutdown_drain_progress",
  "activeRequests": 3
}

and finally:

JSON
{
  "event": "shutdown_complete",
  "activeRequests": 0,
  "durationMs": 4832
}

Useful metrics include:

TEXT
shutdown_duration
requests_active_at_shutdown
requests_completed_during_drain
requests_aborted_during_shutdown
forced_termination_count

Those signals let operators answer questions that a normal Deployment dashboard cannot:

TEXT
How many requests were active when termination began?

How long does drain usually take?

Are Pods reaching zero active requests?

How often are we force-killed?

Do rollout errors correlate with one particular shutdown stage?

If graceful rollout is important enough to engineer, it is important enough to measure.

A More Intentional Deployment Configuration

For a latency-sensitive HTTP service, a reasonable starting structure might resemble:

YAML
apiVersion: apps/v1
kind: Deployment

metadata:
  name: checkout-api

spec:
  replicas: 4

  strategy:
    type: RollingUpdate

    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1

  template:
    metadata:
      labels:
        app: checkout-api

    spec:
      terminationGracePeriodSeconds: 60

      containers:
        - name: app
          image: checkout-api:42

          ports:
            - containerPort: 8080

          startupProbe:
            httpGet:
              path: /startup
              port: 8080

            periodSeconds: 5
            failureThreshold: 24

          readinessProbe:
            httpGet:
              path: /ready
              port: 8080

            periodSeconds: 5
            failureThreshold: 2

          livenessProbe:
            httpGet:
              path: /live
              port: 8080

            periodSeconds: 10
            failureThreshold: 3

          lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sh
                  - -c
                  - sleep 10

This is not a universal production template.

The useful part is being able to explain why each value exists:

TEXT
maxUnavailable:
How much real serving capacity can disappear?

maxSurge:
How much temporary resource overhead is safe?

startupProbe:
How long does real startup require?

readiness:
When can this instance safely take production traffic?

liveness:
When would restart genuinely improve the situation?

preStop:
How much propagation or drain time is required?

terminationGracePeriod:
How long can the complete shutdown lifecycle take?

Configuration should encode measured lifecycle behavior.

It should not be a collection of values copied from another application whose traffic path, request duration, load balancer, and dependencies may be completely different.

Failure Patterns I Would Avoid

Several patterns make me especially nervous because they collapse different lifecycle concerns into one crude signal.

Liveness and readiness mean the same thing

YAML
livenessProbe:
  httpGet:
    path: /health

and:

YAML
readinessProbe:
  httpGet:
    path: /health

with identical semantics.

Temporary inability to accept traffic does not necessarily mean restarting the process will help.

Readiness becomes true when the port opens

TEXT
port opened
=
production ready

even though caches, pools, reference data, or expensive clients are still initializing.

Immediate exit on SIGTERM

TEXT
SIGTERM
→ exit now

with no drain window for active requests or persistent connections.

Grace period shorter than legitimate work

YAML
terminationGracePeriodSeconds: 5

for requests that can take 30 seconds.

The configuration has guaranteed that some valid work cannot complete gracefully.

Huge preStop sleep with no measurement

TEXT
sleep 120 because a blog said so

rather than measuring actual endpoint-removal and drain behavior.

Assuming a PDB prevents downtime

A PDB constrains supported voluntary disruptions.

It does not make nodes or processes immortal.

Treating rollout completion as an application SLO

TEXT
kubectl rollout status = success

does not prove:

TEXT
zero customer-visible failures

The first is Kubernetes object state.

The second is a user-experience property.

The Failure Timeline I Want

A healthy termination path looks something like this:

Diagram
ClientLoad BalancerApplicationEndpoint RoutingKubernetesClientLoad BalancerApplicationEndpoint RoutingKubernetesPropagate backend changeTermination beginsEndpoint terminating / ready=falseEnter drain modeExisting / racing trafficRequestComplete successfullyStop selecting old backendActive requests reach zeroClose resources and exit

The unsafe version changes only one ordering relationship:

Diagram
ClientLoad BalancerApplicationEndpoint RoutingKubernetesClientLoad BalancerApplicationEndpoint RoutingKubernetesRouting state still convergingTermination beginsEndpoint update startsExit immediatelyRequestBackend gone502 / reset

That ordering difference explains how a Deployment can be technically successful while requests still fail.

Kubernetes replaced the Pod correctly.

The traffic handoff was not complete when the process disappeared.

The Invariants I Want

For a Kubernetes HTTP service, I care about guarantees such as:

TEXT
A terminating Pod must stop receiving ordinary new
traffic before its process disappears.

In-flight requests must have enough time to finish
or have explicit retry-safe behavior.

The termination grace period must exceed the real
drain and application-shutdown requirements.

New Pods must not become ready until they can
handle production traffic safely.

Temporary overload must not cause liveness checks
to restart otherwise recoverable Pods.

A rolling update must preserve enough real serving
capacity, not merely enough Ready objects.

External load-balancer draining must be coordinated
with Pod termination.

Planned graceful shutdown must not be the only
mechanism protecting correctness from abrupt failure.

Long-lived connections need an explicit reconnect
or drain policy.

Background workers must stop acquiring new work
before shutdown and finish or safely release the
work they already own.

Sidecars involved in request delivery must remain
available long enough for application drain.

Deployment success must be measured with real user
traffic, not only Kubernetes resource state.

Shutdown must be observable enough to tell whether
requests were drained or merely interrupted.

Those guarantees are much stronger than:

TEXT
strategy: RollingUpdate

because the strategy tells Kubernetes how to replace objects.

The invariants describe what users should experience while that replacement happens.

Final Thought

Kubernetes is very good at replacing Pods.

But a Pod is not a request.

A Deployment controls ReplicaSets. A ReplicaSet controls Pods. EndpointSlices communicate endpoint state. The kubelet manages container termination. A cloud load balancer maintains another view of the available backends. Your application owns active requests and background work. Clients and proxies may retain persistent connections of their own.

A zero-downtime rollout requires those systems to agree on one basic ordering:

TEXT
stop sending new work
↓
finish existing work
↓
terminate process

If the ordering becomes:

TEXT
terminate process
↓
eventually stop sending work

requests get dropped.

The Deployment can still report complete success.

That is why I no longer think of graceful Kubernetes deployment merely as:

“Kubernetes keeps one Pod running while another starts.”

That describes the capacity side of the problem.

The fuller model is:

“The old instance must remain useful until every system that can still route or retain work for it has safely moved on.”

That includes readiness, EndpointSlice state, connection draining, SIGTERM, preStop, termination budgets, external load-balancer propagation, in-flight request tracking, long-lived connections, background workers, sidecars, and enough spare capacity for the rollout itself.

When those pieces are aligned, rolling deployment becomes what people often assume it already guarantees:

a way to change production software without users noticing.

References

  • Kubernetes — Pod Lifecycle. Documents graceful Pod termination, termination grace periods, EndpointSlice behavior during termination, and eventual forced shutdown of containers that remain alive beyond the grace period.

  • Kubernetes — Explore Termination Behavior for Pods and Their Endpoints. Demonstrates endpoint transitions during Pod deletion, including ready: false, serving: true, and terminating: true, and explains how those conditions support connection draining.

  • Kubernetes — EndpointSlices. Defines the ready, serving, and terminating endpoint conditions used by routing and draining implementations.

  • Kubernetes — Deployments. Documents RollingUpdate, maxUnavailable, and maxSurge, including their role in controlling replica replacement.

  • Kubernetes — Liveness, Readiness, and Startup Probes. Defines the separate semantics of readiness, liveness, and startup checks and warns that badly designed liveness checks can create cascading failures under load.

  • Google Kubernetes Engine — Ingress Configuration and Graceful Shutdown. Documents real 502 and 503 failure modes during Pod scale-down and explains how connection draining, preStop, endpoint removal, and termination grace periods need to be coordinated.

  • Kubernetes — PodDisruptionBudget. Documents how PDBs constrain voluntary Pod disruption and why they address a different problem from Deployment rolling-update configuration.