Reported symptoms
A kernel patch reboots db-prod-01 at 03:10. The host is back in ninety
seconds. Every service on it is healthy except PostgreSQL, which has
tried to start three times and given up.
The application reports connection refused — not a timeout, not an
authentication error — so within two minutes the network path and the
firewall are eliminated. The on-call engineer starts the service by hand
and watches it fail the same way, then reboots the host again on the
theory that something did not initialise, and watches it fail the same
way a third time.
The data directory is present and owned by postgres. The filesystem is
41% full. The paired host db-prod-02 restarted an hour earlier in the
same patch window and came back cleanly. Nothing has been deployed to
either host in six weeks, and the change calendar for the night contains
one entry: the kernel patch.
Evidence provided
The end of the cluster log:
$ tail -4 /var/log/postgresql/postgresql-18-main.log2026-08-28 03:11:44.201 UTC [2841] LOG: listening on IPv4 address "0.0.0.0", port 5432
2026-08-28 03:11:44.204 UTC [2841] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2026-08-28 03:11:44.209 UTC [2841] LOG: invalid value for parameter "log_min_duration_statement": "never"
2026-08-28 03:11:44.209 UTC [2841] FATAL: configuration file "/etc/postgresql/18/main/conf.d/30-tuning.conf" contains errorsIllustrative output
The cluster registry:
$ pg_lsclustersVer Cluster Port Status Owner Data directory Log file
18 main 5432 down postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.logIllustrative output
The offending file is dated six weeks earlier. Its modification time matches change ticket CHG-4471, “adjust slow query logging threshold”, which was reloaded, verified and closed on the same day.
And the log from that day:
$ grep -A1 'invalid value' /var/log/postgresql/postgresql-18-main.log.6.gz2026-07-17 14:22:08.551 UTC [1204] LOG: received SIGHUP, reloading configuration files
2026-07-17 14:22:08.552 UTC [1204] LOG: invalid value for parameter "log_min_duration_statement": "never"
2026-07-17 14:22:08.552 UTC [1204] LOG: configuration file "/etc/postgresql/18/main/conf.d/30-tuning.conf" contains errors; unaffected changes were appliedIllustrative output
Work the evidence before reading on
Four questions, and the fourth is the one that matters.
- The server bound its sockets and then shut down. What does that tell you about how far startup got, and what it rules out?
- The same message appears six weeks earlier at severity
LOGand tonight at severityFATAL. Why is it different? - The engineer who made the change verified that
log_min_duration_statementwas 250 afterwards, and it was. What did that verification actually prove? db-prod-02restarted cleanly an hour earlier. Is that reassuring?
Root cause
The value was never valid, and the reload said so
log_min_duration_statement takes a duration — a number of
milliseconds, or a value with a unit, or -1 to disable. The string
never is not any of those.
When the reload met it, the server did three things: it rejected that one line, it applied every other change in the file set, and it kept the value the parameter already had. Then it logged the problem and carried on serving traffic, because that is what a reload is supposed to do.
The verification checked the right value for the wrong reason
The engineer checked log_min_duration_statement after the reload and
found 250. That looked like confirmation. It was a coincidence.
The value 250 was already in force, set months earlier by an
ALTER SYSTEM that wrote it into postgresql.auto.conf. The reload had
changed nothing, so of course the value was still 250. Checking the value
alone cannot distinguish “my change was applied” from “my change was
rejected and the old value survived”.
The paired host was not reassuring, it was a second defect
db-prod-02 restarted cleanly because the file was never written to it.
The change had been applied to one node of a pair and never propagated,
and had been in that state for six weeks.
That is not a mitigation. It means the two nodes of the pair have had divergent configuration for six weeks, and it means the fleet-wide verification that should have caught the broken file also never ran.
Resolution
Correct the file. The intent was a slow-query threshold, so '250ms' is
almost certainly what was meant; if the ticket does not say, delete the
line, which restores exactly the behaviour the server has actually been
running with since July.
sed -i "/log_min_duration_statement/d" /etc/postgresql/18/main/conf.d/30-tuning.conf
pg_ctlcluster 18 main start
tail -3 /var/log/postgresql/postgresql-18-main.log
Confirm from the log that the server reached
database system is ready to accept connections, and confirm with an
actual connection. A start command that returns is not evidence; the
pg_ctl wrapper can report success while the startup process fails
immediately afterwards.
Then, before closing the incident, run the pre-flight query on every server in the estate:
SELECT sourcefile, sourceline, name, setting, error
FROM pg_file_settings WHERE error IS NOT NULL;
Every row is a server that will not restart. Finding them now, while everyone is already awake and the context is fresh, is considerably cheaper than finding them one at a time over the following months.
Two more files can stop a startup the same way:
SELECT line_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL;
SELECT line_number, error FROM pg_ident_file_mappings WHERE error IS NOT NULL;
Note that pg_hba.conf behaves differently again: a single bad line
causes the entire file to be rejected on reload, with
pg_hba.conf was not reloaded, rather than the good lines being applied.
Verification
The cluster is online, the log ends with
database system is ready to accept connections, and a connection
succeeds.
All three pre-flight queries return zero rows.
pg_settings reports log_min_duration_statement with a sourcefile
that is the file you intended, at the value you intended.
And — the only verification that tests what actually failed — a deliberate restart, performed once while somebody is watching, succeeds.
Prevention
Add the pre-flight query to the configuration-change procedure. One query after every reload, and a row in the result means the change failed regardless of what the reload returned.
Monitor it. It is cheap enough to run every minute. An alert on it tells you a server cannot restart, during the entire window in which that is a latent problem rather than an outage.
Verify by source, never by value. This is the specific habit that
would have caught this in July.
Alert on configuration divergence between paired nodes. Six weeks of drift is a defect in its own right, and here it was the only reason the outage was not twice as large.
Restart on a schedule. Every node, one at a time, in a planned window, often enough that no node accumulates months of unexercised configuration. A restart nobody is watching is how a six-week-old typo becomes a 03:10 page.