
CrashLoopBackOff: Why Databases Crash-Loop on Kubernetes and How to Fix It
CrashLoopBackOff is one of the most-searched Kubernetes errors, and almost every article about it is written for stateless apps: check the logs, fix the bug, redeploy. That advice falls apart for databases. A database container carries state across restarts, so a crash loop can be caused by what happened during the previous crash - and a careless fix can turn a five-minute outage into lost data.
So instead of another generic list, we deployed PostgreSQL 18 and MySQL 8.4 as plain StatefulSets on Kubernetes 1.37, broke them on purpose 22 different ways, and recorded what each failure looked like: the pod status, lastState, events, the exact log line, and whether the obvious fix really fixed it.
Every manifest, script and raw log is in blog-data if you want to reproduce any of it.
TL;DR
CrashLoopBackOffis not an error, it’s a restart timer. The real reason is inlastState.terminatedandkubectl logs --previous. Restarts are delayed 10s, 20s, 40s, 80s, 160s, then every 5 minutes.kubectl get eventsnever said “OOMKilled” in any of our memory tests. Only the container’slastStatedid.- Exit code 137 doesn’t always mean out of memory.
OOMKilled/137is the kernel;Error/137is the kubelet killing a container that failed its liveness probe. A liveness-killed PostgreSQL even reportsCompleted/0. - The first boot is the most dangerous moment. If the container dies while the image entrypoint is still initializing, the next start skips initialization. In our tests that left MySQL permanently crash-looping even after the config was fixed, PostgreSQL Ready but refusing every remote connection, and MySQL Ready with a passwordless root and a half-loaded seed.
- Some loops can never fix themselves: OOM during crash recovery, disk full during crash recovery, or a liveness probe shorter than recovery. Each restart replays WAL from the same point and dies at the same place.
- Not everything that looks like a crash loop is one. A missing Secret gives
CreateContainerConfigError; a missing StorageClass givesPending.
What CrashLoopBackOff actually means
A pod is in CrashLoopBackOff when one of its containers keeps exiting and the kubelet is waiting before it restarts the container again. It’s a state, not a cause. We measured the delays on Kubernetes v1.37 with a PostgreSQL pod that crashed on every start:
That’s the classic 10-second backoff, doubling each time and capped at 5 minutes (back-off 5m0s restarting failed container). Two practical consequences:
- After you fix the cause, the pod may sit idle for up to 5 minutes before trying again.
kubectl delete pod <name>gets you a fresh pod with a reset counter; the StatefulSet recreates it with the same PVC. - Newer Kubernetes versions let you tune this. Since 1.35,
KubeletCrashLoopBackOffMaxis on by default, so a cluster admin can lower the cap per node withcrashLoopBackOff.maxContainerRestartPeriodin the kubelet config. The faster default curve (ReduceDefaultCrashLoopBackOffDecay: 1s initial, 60s max) is still an alpha feature, off by default.
Triage a crash-looping database in one minute
Four things tell you almost everything: the container’s last termination, the events, the logs of the previous container, and the pod spec. We wrapped them into one script and used it for every test in this post:
#!/usr/bin/env bash
# crashloop-triage.sh <pod> [namespace]
POD="$1"; NS="${2:-default}"; K="kubectl -n $NS"
$K get pod "$POD" -o wide
$K get pod "$POD" -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\trestarts="}{.restartCount}{"\tlast="}{.lastState.terminated.reason}{"/"}{.lastState.terminated.exitCode}{"\n"}{end}'
$K get events --field-selector "involvedObject.name=$POD" --sort-by=.lastTimestamp | tail -15
$K logs "$POD" --previous --tail=25
kubectl logs without --previous shows the container that is currently starting (or waiting), which is usually empty or mid-startup. The crash is almost always in --previous.
Then read the last= field. Pick the one you’re seeing:
The setup
A single-node k3d cluster running Kubernetes v1.37.1 (containerd 2.3, cgroup v2), local-path storage, and the official images: postgres:18 (18.6), postgres:17, mysql:8.4 (8.4.13), mysql:8.0 and mysql:9.5. No operator - these are the plain StatefulSets people copy from tutorials and Helm charts.
Every test started from a known-good baseline and changed exactly one thing. Here’s the PostgreSQL one, trimmed. It’s also our recommended starting point, for reasons the rest of this post will make obvious:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pg
spec:
serviceName: pg
replicas: 1
selector:
matchLabels: { app: pg }
template:
metadata:
labels: { app: pg }
spec:
securityContext:
fsGroup: 999
containers:
- name: postgres
image: postgres:18
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef: { name: pg-secret, key: POSTGRES_PASSWORD }
resources:
requests: { cpu: 500m, memory: 512Mi }
limits: { cpu: 500m, memory: 512Mi }
volumeMounts:
- name: data
mountPath: /var/lib/postgresql # not .../data on 18+
startupProbe:
exec: { command: ["pg_isready", "-U", "postgres", "-h", "127.0.0.1"] }
periodSeconds: 5
failureThreshold: 60 # up to 5 min for initdb / recovery
livenessProbe:
exec: { command: ["pg_isready", "-U", "postgres", "-h", "127.0.0.1"] }
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 2Gi } }
The MySQL baseline is the same shape with mysql:8.4, /var/lib/mysql, and mysqladmin ping -h 127.0.0.1 as the probe.
OOMKilled: the database outgrew its memory limit
The most common cause of a crash-looping database, and the one with the least helpful signals.
PostgreSQL: shared_buffers larger than the memory limit
We gave PostgreSQL shared_buffers=512MB inside a 256Mi container, along with the kind of WAL tuning people copy from blog posts (max_wal_size=4GB, checkpoint_timeout=30min). It started without complaint - shared memory only counts against the cgroup once pages are actually touched. Then we loaded 4 million rows with pgbench -i -s 40, and it was killed while building the primary key:
creating primary keys...
command terminated with exit code 137
lastState: {"exitCode":137,"reason":"OOMKilled"}
The restart is where it gets nasty. PostgreSQL runs crash recovery, recovery reads pages into the same oversized buffer pool, and the container is OOM-killed again. And again. No client was connected:
pg-0 0/1 CrashLoopBackOff 4 (17s ago) 3m13s
LOG: database system was interrupted while in recovery at 2026-11-04 06:49:37 UTC
HINT: This probably means that some data is corrupted and you will have to use the last backup for recovery.
LOG: redo starts at 0/1761998
Every attempt started redo at the same LSN, so no progress was ever made. Meanwhile kubectl get events only showed Startup probe failed and BackOff. The word “OOMKilled” appeared nowhere except lastState.
The fix is to make the server fit, not to wait it out. We lowered shared_buffers to 128MB - half the limit - and it still got OOM-killed twice more during recovery before the third attempt finished (redo done ... elapsed: 2.97 s). WAL reads and dirty page cache count against the same cgroup. All 4,000,000 rows were intact afterwards, but the lesson is that half the limit is not enough headroom for a recovering PostgreSQL in a small pod. Start with shared_buffers at around 25% of the memory limit.
MySQL: innodb_buffer_pool_size larger than the memory limit
Same idea: innodb_buffer_pool_size=1G in a 512Mi container. MySQL was OOM-killed four seconds into its very first initialization, then came up 1/1 Running on the next restart. That looked like good news until we tried to log in:
$ mysql -uroot -p"$PASSWORD" -h127.0.0.1
ERROR 1130 (HY000): Host '127.0.0.1' is not allowed to connect to this MySQL server
$ mysql -uroot -e "select user, host from mysql.user" # no password at all
user host
mysql.infoschema localhost
mysql.session localhost
mysql.sys localhost
root localhost
The kill landed after mysqld --initialize-insecure finished but before the entrypoint set the root password and created root@'%'. On restart the data directory wasn’t empty, so the entrypoint skipped initialization. The result was a Ready pod with a passwordless root that no application could reach.
Idle, that server already used 496 MiB of anonymous memory against a 512 MiB limit. Any write tipped it over: a simple create-and-insert loop produced OOMKilled, then CrashLoopBackOff, within seconds.
The fix: keep innodb_buffer_pool_size around 50-60% of the container limit for small pods. Connections, per-session buffers, and the redo log all need room too.
MySQL: innodb_dedicated_server ignores your container limit
innodb_dedicated_server=ON sounds like the right switch for “this container is only for MySQL.” We tested it with a 512 MiB memory limit on an 8 GB machine:
| Image | innodb_buffer_pool_size | innodb_redo_log_capacity |
|---|---|---|
| mysql:8.4 | 5376 MB | 5120 MB |
| mysql:8.0 | 6144 MB | 3072 MB |
It sizes itself from host RAM, not the container’s limit. On Kubernetes the first boot was OOM-killed partway through --initialize, and every attempt after that failed with one of the most-searched MySQL errors:
[Entrypoint]: Initializing database files
[ERROR] [MY-010457] [Server] --initialize specified but the data directory has files in it. Aborting.
[ERROR] [MY-013236] [Server] The designated data directory /var/lib/mysql/ is unusable. You can remove all files that the server added to it.
That one never fixes itself. Turn innodb_dedicated_server off, set the buffer pool explicitly, and delete the PVC (it was never initialized, so there’s nothing to lose).
Failed probes: Kubernetes kills a healthy database
A liveness probe that’s tuned for a stateless web app is one of the most reliable ways to crash-loop a database, because databases have legitimately slow moments: first initialization, crash recovery, and heavy queries on a CPU-limited pod.
Liveness probe shorter than crash recovery
We generated 811 MB of WAL in PostgreSQL, killed the postmaster with SIGKILL - the equivalent of a node crash - and brought it back in a pod limited to 200m CPU. The pod had no startup probe and a typical liveness probe (initialDelaySeconds: 10, periodSeconds: 5, failureThreshold: 3), which gives the server about 25 seconds to answer.
07:17:08 0/1 Running 0
07:17:33 0/1 Running 1
07:17:58 0/1 Running 2
07:18:22 0/1 Running 3
07:18:47 0/1 Running 4
07:19:15 0/1 CrashLoopBackOff 4
Each attempt logged the same thing:
LOG: redo starts at 0/1761998
LOG: redo in progress, elapsed time: 20.08 s, current LSN: 0/1DC83C50
FATAL: the database system is not yet accepting connections
DETAIL: Consistent recovery state has not been yet reached.
LOG: received fast shutdown request
Same start position, killed about 20 seconds in, every time. With a startupProbe added, recovery completed in 113 seconds: four and a half times the liveness window. No amount of waiting would have gotten the original pod out of that loop.
Note the termination record for these kills: Completed, exit code 0. PostgreSQL treats SIGTERM as a fast shutdown and exits cleanly, so a pod can crash-loop for an hour without ever showing an error in lastState.
Probe timeouts under a CPU limit
On our first attempt at that test, the 200m CPU pod was killed before we even crashed it - while loading data:
Liveness probe failed: command timed out: "pg_isready -U postgres -h 127.0.0.1" timed out after 1s
Killing: Container postgres failed liveness probe, will be restarted
And once recovery finished, the first count(*) got the server killed again for the same reason. timeoutSeconds defaults to 1 second, and a busy database under a tight CPU limit can’t always schedule pg_isready in time. The fix that finally kept this pod up was a startupProbe plus a tolerant liveness probe: timeoutSeconds: 5, failureThreshold: 6. With that, all 6,000,000 rows were present and the pod stayed up under load.
Liveness probe during first initialization
MySQL’s entrypoint initializes the data directory using a temporary server started with --skip-networking. So a liveness probe that pings 127.0.0.1 fails during init, by design. Without a startup probe, that’s a kill. Here’s what happened with a 60-second seed script in /docker-entrypoint-initdb.d (standing in for loading a dump):
07:06:51 Killing: Container mysql failed liveness probe, will be restarted
... SIGKILL 30s later, in the middle of the seed script.
$ kubectl get pod mysql-0
mysql-0 1/1 Running 1 (72s ago) 2m1s
$ mysql -e "select * from app.customers"
id name
1 first
The entrypoint is a bash script running as PID 1, and it doesn’t handle SIGTERM, so the kubelet’s polite request was ignored until the 30-second grace period ran out. Then came the SIGKILL, mid-seed. After the restart, the data directory existed, initialization was skipped, and the pod went Ready with half the seed data and no error anywhere. In the variant without a seed script, init happened to finish within the grace period and nothing was lost - by timing luck alone.
The fix for all three: always give a database a startupProbe with a generous failureThreshold. Liveness probes don’t run until the startup probe has passed, and that’s exactly the protection initialization and crash recovery need.
Bad Secrets: credentials that don’t match
Missing Secret: not actually a crash loop
If the Secret referenced in secretKeyRef doesn’t exist, the container is never created:
pg-0 0/1 CreateContainerConfigError 0 59s
Warning Failed Error: secret "pg-secrets" not found
Worth knowing because it rules things out: nothing crashed, and there are no logs to read. Create the Secret or fix the name, and the pod recovers on its own.
Empty password or wrong key name
These two do crash-loop, with Error/1 and a clear message. An empty value - for example a Secret created from an unset shell variable - stops PostgreSQL:
Error: Database is uninitialized and superuser password is not specified.
A Secret that uses the wrong key name (here MYSQL_PASSWORD, the application user’s variable, instead of MYSQL_ROOT_PASSWORD) stops MySQL:
[ERROR] [Entrypoint]: Database is uninitialized and password option is not specified
Both only matter on first boot - once the data directory exists, the images ignore these variables entirely. Which leads to the subtle one.
Rotating the password in the Secret
POSTGRES_PASSWORD and MYSQL_ROOT_PASSWORD are read once, when the data directory is initialized. Changing the Secret later does not change the database password. It only changes what everyone thinks the password is.
We rotated the Secret on a running PostgreSQL. Nothing happened, because environment variables are only read when a container starts. Then we deleted the pod, as a node drain or rollout would. With a liveness probe that logs in using the password from the Secret:
Liveness probe failed: psql: error: connection to server at "10.42.0.91", port 5432 failed:
FATAL: password authentication failed for user "postgres"
Killing: Container postgres failed liveness probe, will be restarted
...
pg-0 0/1 CrashLoopBackOff 9 (44s ago) 15m
lastState: Completed/0
A crash loop whose last exit is a clean 0, triggered by a Secret change that, in real life, may have happened days before the pod was next restarted.
Two details made this harder to reproduce than expected, and they’re worth knowing. First, our probe initially used psql -h 127.0.0.1, and nothing broke: the official PostgreSQL image’s pg_hba.conf trusts 127.0.0.1, so localhost never checks the password. Only clients over the network (your applications) fail. Second, mysqladmin ping exits 0 even on Access denied, which makes it safe across password changes. In general, probes should check liveness, not credentials. To rotate a password, run ALTER ROLE / ALTER USER in the database, then update the Secret.
Storage and PVC problems
PVC stuck Pending: also not a crash loop
A volumeClaimTemplate that asks for a StorageClass that doesn’t exist (we used fast-ssd) leaves the pod unscheduled:
pg-0 0/1 Pending 0 59s
Warning FailedScheduling 0/1 nodes are available: pod has unbound immediate PersistentVolumeClaims.
Check kubectl get pvc and kubectl describe pvc. The usual causes are a typo in storageClassName, no default StorageClass, a storage quota, or a volume in a different zone from the node.
lost+found in the data directory
A freshly formatted ext4 block volume - EBS, Persistent Disk, Azure Disk, Ceph RBD - always has a lost+found directory at its root. Mount it directly as PGDATA and initdb refuses:
initdb: error: directory "/var/lib/postgresql/data" exists but is not empty
initdb: detail: It contains a lost+found directory, perhaps due to it being a mount point.
initdb: hint: Using a mount point directly as the data directory is not recommended.
Point PGDATA at a subdirectory, such as /var/lib/postgresql/data/pgdata. MySQL 8.4, for the record, ignored lost+found and initialized fine.
PostgreSQL 18 changed the data path
Starting with the postgres:18 image, PGDATA is /var/lib/postgresql/18/docker, and the recommended mount point is /var/lib/postgresql. Most existing manifests, charts and tutorials still mount at /var/lib/postgresql/data. On a brand-new, empty volume, that crash-loops with:
Error: in 18+, these Docker images are configured to store database data in a
format which is compatible with "pg_ctlcluster" (specifically, using
major-version-specific directory names). ...
Counter to that, there appears to be PostgreSQL data in:
/var/lib/postgresql/data (unused mount/volume)
This is usually the result of upgrading the Docker image without
upgrading the underlying database using "pg_upgrade" ...
The message talks about existing data and upgrades, but there was no data. The entrypoint treats any mount at the old path as a leftover. Change the mountPath to /var/lib/postgresql (or set PGDATA explicitly if you’re migrating an existing volume).
Volume permissions
A non-root PostgreSQL (runAsUser: 999) on a volume whose root is owned by root, with no fsGroup:
mkdir: cannot create directory '/var/lib/postgresql/18': Permission denied
Set securityContext.fsGroup: 999 at the pod level, so Kubernetes makes the volume group-writable on mount.
Disk full
Running out of space in a data file is handled gracefully: the statement fails with ERROR: could not extend file ... No space left on device and everything else keeps working. Running out of space for WAL is not. We simulated the classic cause - a replication slot whose replica never came back, so PostgreSQL keeps every WAL segment - on a small volume:
PANIC: could not write to file "pg_wal/xlogtemp.753": No space left on device
The server restarted, crash recovery started replaying WAL, and recovery needed disk space too:
LOG: redo starts at 0/1761998
FATAL: could not extend file "base/5/16403": No space left on device
CONTEXT: WAL redo at 0/181BCF8 for Heap2/MULTI_INSERT+INIT ...
LOG: startup process (PID 34) exited with exit code 1
LOG: shutting down due to startup process failure
Now you can’t connect to drop the slot, because the server never starts. The way out is to expand the PVC (your StorageClass needs allowVolumeExpansion: true), let PostgreSQL start, then run SELECT pg_drop_replication_slot('...'). Don’t delete files from pg_wal by hand.
Config errors: the server refuses to start
PostgreSQL: typos and missing extensions
The friendliest crash loops. A misspelled parameter:
FATAL: unrecognized configuration parameter "max_conections"
And shared_preload_libraries pointing at an extension that isn’t in the image (common when switching from a custom image back to the official one):
FATAL: could not access file "pg_cron": No such file or directory
Fix the value and the pod starts. Except in one case: we deployed the typo on a fresh volume. initdb succeeded, then the entrypoint’s next step used the same bad arguments and failed. After we fixed the typo, the pod went 1/1 Running with all probes green, and every client got:
FATAL: no pg_hba.conf entry for host "10.42.0.23", user "postgres", database "postgres", no encryption
The entrypoint never got far enough to add the host all all all scram-sha-256 line to pg_hba.conf, and the restart skipped initialization. POSTGRES_USER, POSTGRES_DB and any init scripts were skipped too.
MySQL: a removed variable poisons the data directory
query_cache_size was removed in MySQL 8.0, but it still turns up in configs copied from older setups. On a fresh volume, the first boot failed during initialization:
[Entrypoint]: Initializing database files
[ERROR] [MY-000067] [Server] unknown variable 'query_cache_size=0'.
[ERROR] [MY-013236] [Server] The designated data directory /var/lib/mysql/ is unusable. You can remove all files that the server added to it.
We removed the line from the ConfigMap and restarted the pod. It kept crash-looping:
[ERROR] [MY-010326] [Server] Fatal error: Can't open and lock privilege tables: Table 'mysql.user' doesn't exist
[ERROR] [MY-010952] [Server] The privilege system failed to initialize correctly.
The half-written data directory is permanent. Once it exists, the only fix is to delete the PVC and initialize again. On a new database that costs nothing, which is exactly why it should be your first move here, rather than an hour of editing my.cnf.
Version changes: upgrades and rollbacks
Changing the image tag is the easiest way to crash-loop a healthy database. All three of these failed immediately and safely - nothing had been written yet - but the error messages are worth recognizing.
PostgreSQL major version bump
postgres:17 to postgres:18 on the same volume:
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 17, which is not compatible with this version 18.6 (Debian 18.6-1.pgdg13+2).
Major versions need pg_upgrade, a dump and restore, or logical replication. Setting the image back to 17 recovered the pod with data intact.
MySQL rollback after an upgrade
mysql:8.4 “rolled back” to mysql:8.0:
[ERROR] [MY-014061] [InnoDB] Invalid MySQL server downgrade: Cannot downgrade from 80413 to 80046. Downgrade is only permitted between patch releases.
[ERROR] [MY-010020] [Server] Data Dictionary initialization failed.
Once 8.4 has started on a data directory, the data dictionary is upgraded. Going back to 8.0 means restoring a backup taken before the upgrade, so take one.
MySQL skipping the LTS release
mysql:8.0 straight to mysql:9.5:
[ERROR] [MY-014060] [Server] Invalid MySQL server upgrade: Cannot upgrade from 80046 to 90500. Upgrade to next major version is only allowed from the last LTS release, which version 80046 is not.
The supported path is 8.0 to 8.4 LTS, then 9.x. MySQL refuses before changing anything, so setting the image back to 8.0 recovered the pod.
The pattern behind the worst cases
Line up the scariest results from this post and they share one root cause. Both official images initialize the database in their entrypoint and decide whether to do so with a single question: is the data directory empty? If the container dies during initialization - from a config typo, an OOM kill, or a liveness probe - the next start finds a non-empty directory and skips initialization entirely.
| Interrupted by | Engine | What you got after the restart |
|---|---|---|
| Config typo | PostgreSQL | Ready, probes green, every remote connection rejected |
| Removed variable | MySQL | Permanent crash loop, even after fixing the config |
| OOM kill | MySQL | Ready, root has no password, remote root does not exist |
| OOM kill | MySQL | Permanent --initialize specified but the data directory has files in it |
| Liveness kill | MySQL | Ready, seed data half loaded, no error anywhere |
Some of these crash loops are just noisy. The ones that come back Ready are the dangerous ones. So if a new database crash-looped even once before it went Ready, don’t trust it: delete the PVC and let it initialize cleanly. On day one, that costs you nothing.
A prevention checklist
Everything here came from a test above:
- Add a
startupProbeto every database container, with room for initialization and crash recovery (failureThreshold × periodSecondsof several minutes). - Make liveness tolerant:
timeoutSecondsof 5 or more andfailureThresholdof 6 or more. Probes should check that the process responds, not that credentials work. - Size memory settings from the limit: around 25% for
shared_buffersand 50-60% forinnodb_buffer_pool_sizeon small pods. Never useinnodb_dedicated_serverin a container. - Set
requests == limitsfor memory on databases, and alert onlastState.terminated.reason == OOMKilled, not on events. - Mount PostgreSQL 18+ at
/var/lib/postgresql; on older versions, putPGDATAin a subdirectory of the volume. - Set
fsGroupwhen running as non-root. - Use an expandable StorageClass, and alert on disk usage and inactive replication slots well before 100%.
- Rotate passwords in the database first, then in the Secret.
- Pin image tags, and treat a major version change as a migration, not a rollout.
- If a fresh database crash-looped during first boot, wipe and re-init instead of debugging it.
How operators, OpenEverest and Solanica help
Most of these failures come from plain StatefulSets doing database lifecycle work in a shell script. A Kubernetes operator moves that logic into a controller that understands the database. Bootstrap, probes, configuration and upgrades become managed steps, not side effects of an entrypoint. That removes most of the “half-initialized first boot” class entirely.
OpenEverest builds on that. It runs PostgreSQL and MySQL (along with MongoDB and other engines) through operators, with resource presets for sizing, engine parameters managed per database, guarded version changes (database downgrades are blocked), storage scaling for the disk-full case, and scheduled backups with point-in-time recovery - so “delete the PVC and start over” is never your only option. It’s open source and a CNCF Sandbox project.
Solanica Platform is the enterprise distribution of OpenEverest, with 24/7 support from the engineers who build it. When a production database is crash-looping at 3 a.m., that means a direct line to someone who has already seen the error message.
FAQ
How do I fix CrashLoopBackOff for a PostgreSQL or MySQL pod?
Run kubectl logs <pod> --previous and read the last FATAL or [ERROR] line, then check lastState.terminated.reason in kubectl get pod -o yaml. OOMKilled means memory, Completed/0 or Error/137 with a Killing event means a probe, and Error/1 means the database refused to start for the reason in the log.
What does exit code 137 mean for a database pod?
The process received SIGKILL. With reason OOMKilled it was the kernel enforcing the memory limit. With reason Error it was usually the kubelet, after a failed liveness probe and an expired termination grace period.
How long does Kubernetes wait between CrashLoopBackOff restarts? 10 seconds, doubling each restart, capped at 5 minutes by default. Deleting the pod resets the counter.
Can CrashLoopBackOff cause data loss? The restarts themselves don’t delete data. But in our tests, a container killed during first initialization left databases that were missing users, network access or seed data, and a liveness probe shorter than crash recovery kept a database offline indefinitely. Wiping a PVC to “fix” a crash loop on an existing database is data loss, so confirm your backups first.
Why is my Postgres 18 pod crash-looping when Postgres 17 worked?
Most likely the volume is mounted at /var/lib/postgresql/data. The postgres:18 image expects a mount at /var/lib/postgresql and refuses to start otherwise, even on an empty volume.
Try it yourself
All 22 manifests, the triage script and the raw output are in blog-data. If you’re fighting a crash-looping database right now, or want a second pair of eyes on your probes and sizing before it happens, book a call with our team.


