Readiness and liveness answer different operational questions. Readiness asks whether this container should contribute to a Pod that accepts ordinary Service traffic now. Liveness asks whether restarting this container is the configured recovery action. Both can use HTTP, but sharing a protocol does not make their meanings interchangeable. Define the action you want before choosing the condition that triggers it.

Observe the action as well as the response

A readiness failure makes the container unready and affects Pod readiness. For a normal selector-based Service, endpoint reconciliation then changes which backends are eligible for ordinary routing. The Pod can remain Running while it is not Ready, and the application process may continue executing. Restart count and Pod UID answer different questions from the Ready condition.

Liveness failures reaching the configured threshold cause kubelet to terminate the container and apply its restart behavior. In a usual Always-policy application Pod this produces a replacement container inside the same Pod. It does not inherently create a new Pod object or deploy another application version. Compare container restart count and termination state alongside the Pod identity.

Use inexpensive checks with meaningful success criteria. A TCP connection proves that a listener accepts connections, not that an application can complete useful work. An HTTP endpoint can answer a richer question, but can also create load if it recursively checks expensive dependencies. Keep the probe’s own execution cost within the workload’s capacity and timeout budget.

When it breaks

Treating a shared database outage as a liveness failure can repeatedly restart every healthy application process without repairing the database. Adding that dependency to readiness also needs judgment: withdrawing every replica may remove useful fallback responses or overload the remaining replicas during a partial failure. Define readiness from the service contract, including any deliberately supported degraded behavior.

Readiness is not a network access control. A Pod address can remain reachable when the Pod has no ready Service target. Existing connections, other routing layers, and Service options such as publishNotReadyAddresses also require their own analysis. Do not describe a failed readiness probe as an instantaneous guarantee that no request can ever reach the process.

Thresholds and timing control sensitivity. A short timeout can turn CPU contention into apparent failure; several consecutive failures can avoid reacting to one brief delay. Startup protection is separate: a configured startup probe suppresses readiness and liveness checks until it succeeds. These settings are sampling and recovery policy, not a proof of zero lost requests during termination.

Check it quickly

Use a disposable Kubernetes cluster with kubectl access and python:3.11-slim-bookworm available on its nodes. Save the code as probe-actions.py, then run python3 probe-actions.py KUBECONFIG_PATH CONTEXT with explicit test-cluster arguments. The account needs namespace creation and deletion permissions. The recorded server was Kubernetes 1.34.11. The script creates one HTTP Pod and an ordinary Service with default endpoint readiness behavior.

import json
import subprocess
import sys
import time
from uuid import uuid4

assert len(sys.argv) == 3, 'usage: python3 example.py DISPOSABLE_KUBECONFIG CONTEXT'
base = ['kubectl', '--kubeconfig', sys.argv[1], '--context', sys.argv[2], '--request-timeout=30s']
image = 'python:3.11-slim-bookworm'


def k(*args, payload=None, expected=0):
    result = subprocess.run(base + list(args), input=json.dumps(payload) if payload is not None else None,
                            capture_output=True, text=True, timeout=150)
    if expected is not None:
        assert result.returncode == expected, result.stderr
    return result


def obj(namespace, kind, name=None, selector=None):
    args = ['get', kind, '-n', namespace, '-o', 'json']
    if name:
        args.append(name)
    if selector:
        args += ['-l', selector]
    return json.loads(k(*args).stdout)


def apply(namespace, data):
    return k('apply', '-n', namespace, '-f', '-', payload=data)


def wait_for(read, ready, seconds=120):
    until = time.monotonic() + seconds
    while time.monotonic() < until:
        value = read()
        if ready(value):
            return value
        time.sleep(0.5)
    raise AssertionError('expected Kubernetes condition did not arrive')


def pod_ready(pod):
    return any(c['type'] == 'Ready' and c['status'] == 'True'
               for c in pod.get('status', {}).get('conditions', []))


def spec(script='import time; time.sleep(600)'):
    return {'automountServiceAccountToken': False, 'terminationGracePeriodSeconds': 1,
            'containers': [{'name': 'app', 'image': image, 'imagePullPolicy': 'IfNotPresent',
                            'command': ['python3', '-c', script],
                            'resources': {'requests': {'cpu': '10m', 'memory': '24Mi'},
                                          'limits': {'cpu': '200m', 'memory': '96Mi'}}}]}

namespace = 'notes-probes-' + uuid4().hex[:8]
k('create', 'namespace', namespace)
try:
    server = '''from http.server import BaseHTTPRequestHandler,HTTPServer
from pathlib import Path
class Handler(BaseHTTPRequestHandler):
 def do_GET(self):
  flag={'/live':'/tmp/live-fail','/ready':'/tmp/ready-fail'}.get(self.path)
  code=503 if flag and Path(flag).exists() else 200
  body=b'worker'; self.send_response(code); self.send_header('Content-Length',str(len(body)))
  self.end_headers(); self.wfile.write(body)
 def log_message(self,*args): pass
HTTPServer(('0.0.0.0',8080),Handler).serve_forever()
'''
    podspec = spec(server)
    c = podspec['containers'][0]
    c['readinessProbe'] = {'httpGet': {'path': '/ready', 'port': 8080}, 'periodSeconds': 1, 'failureThreshold': 2}
    c['livenessProbe'] = {'httpGet': {'path': '/live', 'port': 8080}, 'periodSeconds': 1, 'failureThreshold': 3}
    apply(namespace, {'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'worker', 'labels': {'app': 'worker'}}, 'spec': podspec})
    apply(namespace, {'apiVersion': 'v1', 'kind': 'Service', 'metadata': {'name': 'web'},
          'spec': {'selector': {'app': 'worker'}, 'ports': [{'port': 80, 'targetPort': 8080}]}})
    def targets():
        return {e['targetRef']['uid'] for s in obj(namespace, 'endpointslices', selector='kubernetes.io/service-name=web')['items']
                for e in s.get('endpoints', []) if e.get('conditions', {}).get('ready') and e.get('targetRef')}
    initial = wait_for(lambda: obj(namespace, 'pod', 'worker'), pod_ready)
    uid = initial['metadata']['uid']
    wait_for(targets, lambda value: value == {uid})
    def change(path, create):
        method = 'touch' if create else 'unlink'
        k('exec', '-n', namespace, 'worker', '--', 'python3', '-c',
          'from pathlib import Path; Path('+repr(path)+').'+method+'()')
    change('/tmp/ready-fail', True)
    wait_for(lambda: obj(namespace, 'pod', 'worker'), lambda p: not pod_ready(p))
    wait_for(targets, lambda value: not value)
    time.sleep(4)
    unready = obj(namespace, 'pod', 'worker')
    assert unready['metadata']['uid'] == uid and unready['status']['containerStatuses'][0]['restartCount'] == 0
    address = 'http://' + unready['status']['podIP'] + ':8080/api'
    probe = 'from urllib.request import urlopen; print(urlopen('+repr(address)+',timeout=3).read().decode())'
    assert k('exec', '-n', namespace, 'worker', '--', 'python3', '-c', probe).stdout.strip() == 'worker'
    change('/tmp/ready-fail', False)
    wait_for(targets, lambda value: value == {uid})
    change('/tmp/live-fail', True)
    restarted = wait_for(lambda: obj(namespace, 'pod', 'worker'), lambda p:
        pod_ready(p) and p['status']['containerStatuses'][0]['restartCount'] >= 1)
    assert restarted['metadata']['uid'] == uid
    assert restarted['status']['containerStatuses'][0]['restartCount'] == 1
    wait_for(targets, lambda value: value == {uid})
    print(json.dumps({'readiness_removed_ready_target_without_restart': True,
                      'unready_pod_direct_http_still_succeeds': True,
                      'readiness_recovery_restores_target': True,
                      'liveness_container_restarts': 1, 'same_pod_uid': True}))
finally:
    k('delete', 'namespace', namespace, '--wait=true', '--timeout=90s')

The initial Pod is Ready and its UID appears in the Service’s ready EndpointSlice targets. Creating a local flag makes only the readiness endpoint return 503. After the failure threshold and reconciliation, the ready-target set is empty. Four seconds later the Pod UID is unchanged and the container restart count remains zero.

A direct HTTP request to that unready Pod’s address still returns worker. Removing the readiness flag restores its ready target. The script then creates a different flag that makes the liveness endpoint fail. Kubelet restarts the container once, its fresh writable layer removes the flag, and the ready target returns. The Pod UID remains the same throughout.

All assertions passed and the namespace was removed. This demonstrates distinct controller actions and direct reachability, not graceful draining of existing connections or production availability. For a real rollout, verify endpoint propagation, application shutdown, outstanding work and client behavior together; a green probe alone cannot establish the whole request lifecycle.

Sources