CrashLoopBackOff: Why Databases Crash-Loop on Kubernetes and How to Fix It

CrashLoopBackOff: Why Databases Crash-Loop on Kubernetes and How to Fix It

• By , founder of Solanica and maintainer of OpenEverest (CNCF Sandbox)

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

  • CrashLoopBackOff is not an error, it’s a restart timer. The real reason is in lastState.terminated and kubectl logs --previous. Restarts are delayed 10s, 20s, 40s, 80s, 160s, then every 5 minutes.
  • kubectl get events never said “OOMKilled” in any of our memory tests. Only the container’s lastState did.
  • Exit code 137 doesn’t always mean out of memory. OOMKilled/137 is the kernel; Error/137 is the kubelet killing a container that failed its liveness probe. A liveness-killed PostgreSQL even reports Completed/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 gives Pending.

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:

  1. 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.
  2. Newer Kubernetes versions let you tune this. Since 1.35, KubeletCrashLoopBackOffMax is on by default, so a cluster admin can lower the cap per node with crashLoopBackOff.maxContainerRestartPeriod in 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:

Imageinnodb_buffer_pool_sizeinnodb_redo_log_capacity
mysql:8.45376 MB5120 MB
mysql:8.06144 MB3072 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 byEngineWhat you got after the restart
Config typoPostgreSQLReady, probes green, every remote connection rejected
Removed variableMySQLPermanent crash loop, even after fixing the config
OOM killMySQLReady, root has no password, remote root does not exist
OOM killMySQLPermanent --initialize specified but the data directory has files in it
Liveness killMySQLReady, 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:

  1. Add a startupProbe to every database container, with room for initialization and crash recovery (failureThreshold × periodSeconds of several minutes).
  2. Make liveness tolerant: timeoutSeconds of 5 or more and failureThreshold of 6 or more. Probes should check that the process responds, not that credentials work.
  3. Size memory settings from the limit: around 25% for shared_buffers and 50-60% for innodb_buffer_pool_size on small pods. Never use innodb_dedicated_server in a container.
  4. Set requests == limits for memory on databases, and alert on lastState.terminated.reason == OOMKilled, not on events.
  5. Mount PostgreSQL 18+ at /var/lib/postgresql; on older versions, put PGDATA in a subdirectory of the volume.
  6. Set fsGroup when running as non-root.
  7. Use an expandable StorageClass, and alert on disk usage and inactive replication slots well before 100%.
  8. Rotate passwords in the database first, then in the Secret.
  9. Pin image tags, and treat a major version change as a migration, not a rollout.
  10. 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.

See Also