PostgreSQL webhook timeout during rolling update in Kubernetes

Problem: Webhook timeout during rolling update of PostgreSQL pods in Kubernetes

During a CI/CD‑driven rolling update of a PostgreSQL StatefulSet (or a Patroni‑managed cluster), the admission webhook that validates pod creation repeatedly fails with a timeout. Typical error messages observed in the controller logs are:


Failed calling webhook "postgresql-admission-webhook": context deadline exceeded
Error from server (InternalError): admission webhook "pg-webhook.k8s.io" denied the request: webhook timeout
Timed out waiting for the condition while waiting for PostgreSQL pod to become ready during rolling update

The failure aborts the rollout, leaves the StatefulSet partially updated, and can cause replication lag or temporary loss of write capability.

Root Cause Analysis

The timeout originates from the interaction of three Kubernetes mechanisms:

  1. Admission webhook request timeout – By default, Kubernetes aborts a webhook call after 30s (see Kubernetes Documentation – Admission Controllers > Webhook Configuration). The failurePolicy setting determines whether a timeout is treated as a failure.
  2. PostgreSQL recovery / replication catch‑up time – When a pod is terminated, the new pod must start PostgreSQL, recover WAL, and re‑establish streaming replication. In production clusters, especially with large WAL volumes or slow disks, this can exceed 30 seconds (see PostgreSQL Documentation – Chapter 27: Streaming Replication and Logical Replication).
  3. Pod termination grace period and readiness probes – Rolling updates respect terminationGracePeriodSeconds and readinessProbe settings. If the readiness probe waits for replication to be in sync, the webhook sees the pod as not ready while it is still performing recovery (see Kubernetes Documentation – Rolling Updates for Deployments).

When the webhook’s timeoutSeconds (default 30 s) expires before the new PostgreSQL pod reports Ready, the webhook returns context deadline exceeded. If failurePolicy: Fail is configured (as in the CrunchyData incident), the rollout is aborted.

Investigation and Debugging

Follow these steps to isolate the failure path:

  1. Inspect webhook configuration

kubectl get mutatingwebhookconfiguration pg-webhook -o yaml

Key fields:

Field Typical Value
timeoutSeconds 30
failurePolicy Fail
  1. Check pod termination and startup logs

kubectl logs -f postgres-0 -n db
# Look for lines such as:
2026-06-02 14:12:03.123 UTC [1] LOG:  database system was shut down at 2026-06-02 14:11:58 UTC
2026-06-02 14:12:05.456 UTC [1] LOG:  starting up replication slot "_some_slot"
2026-06-02 14:12:45.789 UTC [1] LOG:  database system is ready to accept connections

Notice the ~40 s gap between shutdown and readiness – longer than the webhook timeout.

  1. Capture the webhook request/response latency

kubectl get events -n db --field-selector reason=FailedCallingWebhook
# Example output:
LAST SEEN   TYPE      REASON                OBJECT               MESSAGE
2m          Warning   FailedCallingWebhook  pod/postgres-0       context deadline exceeded
  1. Validate replication slot existence

kubectl exec -it postgres-0 -- psql -U postgres -c "SELECT * FROM pg_replication_slots;"

If the slot is missing, the readiness probe will wait for its recreation, extending startup time (see the “Replication slot does not exist” error in community sources).

Resolution

Two complementary categories of fixes are required: extend the webhook timeout and ensure PostgreSQL can become ready within that window.

1. Increase webhook timeout and relax failure policy

Modify the MutatingWebhookConfiguration (or ValidatingWebhookConfiguration) to allow a longer timeout and change failurePolicy to Ignore for non‑critical failures.


# Before
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: pg-webhook
webhooks:
- name: pg-webhook.k8s.io
  clientConfig:
    service:
      name: pg-admission
      namespace: db
      path: /mutate
    caBundle: ...
  timeoutSeconds: 30
  failurePolicy: Fail

# After
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: pg-webhook
webhooks:
- name: pg-webhook.k8s.io
  clientConfig:
    service:
      name: pg-admission
      namespace: db
      path: /mutate
    caBundle: ...
  timeoutSeconds: 60   # extended to cover worst‑case recovery
  failurePolicy: Ignore

2. Adjust PostgreSQL pod lifecycle parameters

  • Increase terminationGracePeriodSeconds to give the old pod enough time to finish WAL streaming. Example:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 120   # previously 30
  • Configure readiness probe to succeed earlier – instead of waiting for full replication catch‑up, probe the PostgreSQL process only:

readinessProbe:
  exec:
    command: ["pg_isready", "-U", "postgres"]
  initialDelaySeconds: 5
  periodSeconds: 10
  failureThreshold: 3

This decouples the webhook’s expectation from replication lag.

  • Pre‑create replication slots using Patroni’s slot_name configuration or an init container so that the new pod does not need to recreate them on start.

# Patroni config snippet
postgresql:
  parameters:
    max_replication_slots: 10
  replication:
    slots:
      - name: _some_slot
        type: physical

Verification

After applying the changes, perform a controlled rolling update and observe the following:

  1. Webhook call completes within the new timeout.
  2. Pod transitions to Ready before the webhook finishes.
  3. No “context deadline exceeded” events appear.

kubectl rollout restart statefulset/postgres -n db
kubectl get pods -w -n db
# Expected output snippet:
postgres-0   1/1     Running   0          2m
postgres-1   1/1     Running   0          2m

Confirm replication health:


kubectl exec -it postgres-0 -- psql -U postgres -c "SELECT client_addr, state FROM pg_stat_replication;"
# Expected rows show state = 'streaming' for all replicas.

Prevention and Operational Guardrails

  • Monitoring – Alert on admission webhook call duration > 45s and on PostgreSQL pod readiness latency > 60s (Prometheus queries).
  • PodDisruptionBudget – Define a PDB that ensures at least N-1 replicas stay healthy during updates (Patroni docs recommend this).
  • CI/CD pipeline guardrails – Add a step that runs kubectl wait --for=condition=Ready pod/<pod-name> --timeout=120s before proceeding to the next rollout stage.
  • Capacity planning – Size storage and CPU to keep recovery time under the webhook timeout threshold; consider wal_keep_segments and checkpoint settings.
  • Documented rollback procedure – Keep a known‑good manifest with the original timeoutSeconds and failurePolicy values for emergency rollbacks.

Related Topic Hub: Data Infrastructure Troubleshooting Hub

FAQ

  1. Why does the webhook timeout only during rolling updates and not on initial deployment?
    During initial deployment the pod starts with an empty data directory, so recovery is fast. Rolling updates replace a running primary; the new pod must replay WAL generated while the old pod was still serving traffic, which can be substantially longer.
  2. Can I keep failurePolicy: Fail and still avoid timeouts?
    Yes, by ensuring the pod becomes ready within the webhook’s timeoutSeconds. This typically requires increasing the timeout, optimizing recovery (e.g., faster disks, pre‑created slots), or adjusting the readiness probe to return earlier.
  3. Is increasing terminationGracePeriodSeconds enough?
    It prevents premature pod kill but does not affect the webhook’s request timeout. Both the grace period and the webhook timeout must be aligned with the worst‑case recovery duration.
  4. Do logical replication slots cause the same issue as physical slots?
    Both can delay readiness if the slot needs to be recreated. Logical slots often involve additional decoding overhead, so they are more likely to exceed the timeout.
  5. How can I debug a “Replication slot does not exist” error that triggers a webhook timeout?
    Check the init container or Patroni configuration that creates the slot. Verify with SELECT * FROM pg_replication_slots; after pod start. If missing, add an init script that runs SELECT pg_create_physical_replication_slot('_some_slot'); before PostgreSQL starts accepting connections.