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:
- Admission webhook request timeout – By default, Kubernetes aborts a webhook call after
30s(see Kubernetes Documentation – Admission Controllers > Webhook Configuration). ThefailurePolicysetting determines whether a timeout is treated as a failure. - 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).
- Pod termination grace period and readiness probes – Rolling updates respect
terminationGracePeriodSecondsandreadinessProbesettings. 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:
- Inspect webhook configuration
kubectl get mutatingwebhookconfiguration pg-webhook -o yaml
Key fields:
| Field | Typical Value |
|---|---|
| timeoutSeconds | 30 |
| failurePolicy | Fail |
- 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.
- 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
- 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
terminationGracePeriodSecondsto 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_nameconfiguration 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:
- Webhook call completes within the new timeout.
- Pod transitions to
Readybefore the webhook finishes. - 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 > 45sand onPostgreSQL pod readiness latency > 60s(Prometheus queries). - PodDisruptionBudget – Define a PDB that ensures at least
N-1replicas 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=120sbefore proceeding to the next rollout stage. - Capacity planning – Size storage and CPU to keep recovery time under the webhook timeout threshold; consider
wal_keep_segmentsand checkpoint settings. - Documented rollback procedure – Keep a known‑good manifest with the original
timeoutSecondsandfailurePolicyvalues for emergency rollbacks.
Related Topic Hub: Data Infrastructure Troubleshooting Hub
FAQ
- 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. - Can I keep
failurePolicy: Failand still avoid timeouts?
Yes, by ensuring the pod becomes ready within the webhook’stimeoutSeconds. This typically requires increasing the timeout, optimizing recovery (e.g., faster disks, pre‑created slots), or adjusting the readiness probe to return earlier. - Is increasing
terminationGracePeriodSecondsenough?
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. - 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. - 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 withSELECT * FROM pg_replication_slots;after pod start. If missing, add an init script that runsSELECT pg_create_physical_replication_slot('_some_slot');before PostgreSQL starts accepting connections.