Skip to main content
RunBook Academy

PostgreSQLIII · Configuration ArchitectureConfiguration

A configuration you can hand over

Intermediate⏱ ~25 minpsql

What you'll learn

  • Lay out configuration so that ownership and intent are visible
  • Distinguish values chosen deliberately from values left at the default
  • Detect drift between the intended configuration and the running server
  • Decide what belongs in a file, in a role setting, and in the application

Prerequisites

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

A PostgreSQL cluster that has been running for four years has a configuration nobody fully understands. Values were set during incidents, copied from a blog post, inherited from a previous host, or chosen carefully by somebody who has left. They all look identical in the file.

The practical consequence is that nobody dares change any of them, because there is no way to tell a load-bearing value from a superstition. That is the state this lesson exists to prevent.

Separate what you chose from what you inherited

The first question about any cluster is short: what is not at its default?

Read-only / Safeevery value somebody actually chose
$ psql -U postgres -c "SELECT name, setting, source FROM pg_settings WHERE source <> 'default' ORDER BY source, name"
            name            |      setting       |       source
----------------------------+--------------------+--------------------
application_name           | psql               | client
autovacuum_worker_slots    | 16                 | configuration file
DateStyle                  | ISO, MDY           | configuration file
default_text_search_config | pg_catalog.english | configuration file
dynamic_shared_memory_type | posix              | configuration file
listen_addresses           | *                  | configuration file
max_connections            | 100                | configuration file
max_wal_size               | 1024               | configuration file
shared_buffers             | 16384              | configuration file
wal_level                  | replica            | command line
config_file                | .../postgresql.conf| override
... (this cluster: 33 rows in total)

That list is the actual configuration. Everything else is upstream’s choice, and upstream’s choices are frequently better than a half-remembered adjustment.

The corollary is worth stating: a parameter you have not deliberately chosen should not appear in your files at all. A postgresql.conf containing two hundred commented and uncommented lines copied from a template obscures the nine decisions that were actually made.

A layout that shows ownership

postgresql.conf                  # stock, plus one line: include_dir = 'conf.d'
conf.d/
├── 10-connections.conf          # max_connections, and why
├── 20-memory.conf               # shared_buffers, work_mem, maintenance_work_mem
├── 30-wal.conf                  # max_wal_size, checkpoint settings
├── 40-autovacuum.conf           # autovacuum thresholds for this workload
├── 50-logging.conf              # what we log and why we do not log more
└── 90-host-overrides.conf       # generated per host; the only machine-specific file

Three properties make this worth the small setup cost.

Ownership is visible in the filename. A change to autovacuum touches one file. A reviewer reading a diff sees the subject before reading the content.

Order is explicit. Files are read in name order and later wins, so 90-host-overrides.conf beating 20-memory.conf is a stated rule rather than an accident of file position.

The stock file stays stock. Upgrades ship a new postgresql.conf with new parameters and updated comments. If your only edit is the include_dir line, adopting the new one is trivial. If your decisions are scattered through it, every upgrade is a merge.

Record the reason, not just the value

A value without a reason cannot be reviewed, and reviewing is the whole point of writing it down.

# 30-wal.conf

# max_wal_size raised from the 1GB default on 2026-08-27.
#
# Why: pg_stat_checkpointer showed num_requested at 2,847 against
#      num_timed at 41, so checkpoints were being forced by WAL volume
#      rather than paced by checkpoint_timeout. The bulk ingestion job
#      introduced in July generates roughly 6GB of WAL per hour.
#
# Trade: recovery after an unclean shutdown replays more WAL, measured
#        at ~90s on this hardware. Accepted; documented in the DR plan.
#
# Revisit if: the ingestion job is removed, or pg_wal capacity changes.
max_wal_size = 8GB

Four lines: what changed, the evidence that prompted it, the cost accepted, and the condition that would make it wrong. The last one is the most valuable and the most often omitted — it is what lets a future engineer decide whether the value is still correct without re-deriving the original analysis.

Detecting drift

The intended configuration is in a repository. The running configuration is in a server. They diverge, and the divergence is invisible until it matters.

# What the running server is actually using, in a comparable form
psql -U postgres -tAc \
  "SELECT name || ' = ' || setting
     FROM pg_settings
    WHERE source NOT IN ('default','client','session')
    ORDER BY name" > /tmp/running.txt

# Anything set out of band, bypassing the managed files entirely
psql -U postgres -c \
  "SELECT name, setting, sourcefile FROM pg_settings
    WHERE sourcefile LIKE '%auto.conf'"

# Any per-role or per-database setting, which lives in a catalogue
# rather than in any file and is therefore invisible to a file diff
psql -U postgres -c \
  "SELECT coalesce(d.datname,'(all databases)') AS database,
          coalesce(r.rolname,'(all roles)') AS role, s.setconfig
     FROM pg_db_role_setting s
     LEFT JOIN pg_database d ON d.oid = s.setdatabase
     LEFT JOIN pg_roles r ON r.oid = s.setrole"

The second and third queries are the ones a file-based drift check misses entirely. postgresql.auto.conf is written by ALTER SYSTEM and is not in your repository. pg_db_role_setting is a catalogue table and is not a file at all. Both override the managed configuration, and both are the natural residue of incidents.

Production discipline

  1. Keep postgresql.conf stock plus one include_dir line. It makes every major upgrade an append rather than a merge.
  2. Put decisions in numbered fragments so ownership is visible in the filename and precedence is a stated rule.
  3. Record the evidence, the trade accepted, and the revisit condition beside every non-default value. A value without a reason cannot be reviewed and therefore will never be changed.
  4. Audit with source <> 'default'. That list is the real configuration; everything else is upstream’s choice.
  5. Check postgresql.auto.conf and pg_db_role_setting in every drift check. Neither is a file in your repository and both override it.
  6. Set application_name in every connection string. It costs nothing and it names the session in pg_stat_activity and the log.

Cross-course references

  • Git, CI/CD & GitOps — Part LXXV (Drift) and Part LXXXVIII (Infrastructure GitOps) cover reconciling intended against actual state, which is the general form of this lesson’s drift section.
  • Ansible for Production Sysadmins — Part XVII (Templates) and Part XXXVI (Drift) cover generating the conf.d fragments and detecting divergence from them.
  • Linux for Production Sysadmins — Part LXXIII (Change) and Part LXXIV (Drift) cover the host-level version of the same discipline.

Quiz

Knowledge check · 6 questions

  1. Q1. A drift check compares the configuration files on each database host against the versions in the repository and reports no differences. Which overrides can it still be missing?

  2. Q2. Why is keeping postgresql.conf stock apart from a single include_dir line particularly valuable at major upgrade time?

  3. Q3. Which of these should be recorded alongside a non-default configuration value? Select all that apply.

  4. Q4. A parameter left at its upstream default should generally not appear in your managed configuration files at all.

  5. Q5. Explain why statement_timeout for a reporting workload is better attached to a role than set server-wide, and name one other setting that belongs at the same level.

  6. Q6. Assess the situation and give the sequence you would follow.

    You inherit a PostgreSQL 18 cluster that has been in production for four years across three previous owners. postgresql.conf is 1,400 lines with roughly sixty uncommented parameters, no comments explaining any of them, and a block near the end that appears to have been pasted from a tuning guide. Query latency is acceptable but the team is afraid to change anything. There is no configuration repository. The cluster is due a major version upgrade within the year.

Passing score: 75%. Answers are checked in this browser.