Skip to main content
RunBook Academy

PostgreSQLIII · Configuration ArchitectureConfiguration

The precedence ladder beyond the files

Intermediate⏱ ~25 min🧪 Lab requiredpsql

What you'll learn

  • Order the five levels a setting can be attached to, from least to most specific
  • Read pg_db_role_setting and interpret what each row overrides
  • Use the source column of pg_settings to identify which level supplied a value
  • Apply per-role settings to bound a workload without changing the server default

Prerequisites

Practice

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.

The configuration files decide what the server starts with. They do not decide what a given session actually runs with, because four more levels sit above them, each more specific than the last, and each able to override.

This is why “we set work_mem to 4 MB in postgresql.conf” is not an answer to “how much memory can a query allocate”. It is a statement about the default.

The five levels

flowchart TD
    A["Server configuration\npostgresql.conf + auto.conf"] --> B["Per-database\nALTER DATABASE ... SET"]
    B --> C["Per-role\nALTER ROLE ... SET"]
    C --> D["Per-role-in-database\nALTER ROLE ... IN DATABASE ... SET"]
    D --> E["Session\nSET, or the connection string"]
    E --> F["The value the query uses"]

More specific wins. A per-role setting beats a per-database setting; a session SET beats everything.

Watching it resolve

Two settings were attached at different levels on a live cluster: a work_mem on the shop database, and a statement_timeout on the reporting_ro role.

ALTER DATABASE shop SET work_mem = '32MB';
ALTER ROLE reporting_ro SET statement_timeout = '5min';

Both are stored in one catalogue:

Read-only / Safepg_db_role_setting, joined to readable names
$ psql -U postgres -c 'SELECT coalesce(d.datname, ...) AS database, coalesce(r.rolname, ...) AS role, s.setconfig FROM pg_db_role_setting s ...'
    database     |     role     |        setconfig
-----------------+--------------+--------------------------
shop            | (all roles)  | {work_mem=32MB}
(all databases) | reporting_ro | {statement_timeout=5min}
(2 rows)

The full query is worth keeping, because the raw catalogue stores OIDs and zero rather than names and nulls:

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
    ORDER BY 1, 2"

Now connect as reporting_ro to shop and ask what is in force:

Read-only / Safethe source column names the level that won
$ psql -U reporting_ro -d shop -c "SELECT name, setting, source FROM pg_settings WHERE name IN ('work_mem','statement_timeout')"
       name        | setting |  source
-------------------+---------+----------
statement_timeout | 300000  | user
work_mem          | 32768   | database
(2 rows)

source = database means the per-database setting supplied it. source = user means the per-role setting did — the value name is user for historical reasons and it means role, which is a genuine trap when reading this column quickly.

A session SET then beats both:

Read-only / Safea session SET wins over everything
$ psql -U reporting_ro -d shop -c "SET work_mem='99MB'; SELECT name, setting, source FROM pg_settings WHERE name='work_mem'"
SET
 name   | setting | source
----------+---------+---------
work_mem | 101376  | session
(1 row)

What this is genuinely good for

Per-role settings are the mechanism for bounding a workload without touching anybody else, and they are underused.

-- A reporting role that cannot hold a transaction open all night
ALTER ROLE reporting_ro SET statement_timeout = '5min';
ALTER ROLE reporting_ro SET idle_in_transaction_session_timeout = '60s';

-- ...and may use more working memory, because its queries sort large sets
ALTER ROLE reporting_ro SET work_mem = '64MB';

-- A migration role that may take longer to acquire a lock, but not forever
ALTER ROLE migrator SET lock_timeout = '5s';
ALTER ROLE migrator SET statement_timeout = '30min';

Each of those is a policy attached to an identity, applied at connection time, requiring no application change and no server restart. The statement_timeout and idle_in_transaction_session_timeout pair is the single most valuable application of this feature, and Part IV and Part VII both return to why.

Per-database settings suit properties of the data rather than the consumer — a database whose workload is analytical wanting a different default_statistics_target, for instance.

Removing a setting

Each level has its own reset, and using the wrong one silently does nothing:

ALTER DATABASE shop RESET work_mem;
ALTER ROLE reporting_ro RESET statement_timeout;
ALTER ROLE reporting_ro IN DATABASE shop RESET work_mem;
ALTER ROLE reporting_ro RESET ALL;        -- every setting for that role
RESET work_mem;                            -- this session only

The failure worth knowing: running ALTER DATABASE shop RESET work_mem when the setting was attached to a role removes nothing and reports success. pg_db_role_setting is where you check which level actually holds it, and it is the first thing to read rather than the last.

Production discipline

  1. Read source before concluding anything about a value. It names the level in one word and turns a puzzling number into a located one.
  2. Remember source = user means the role level, not the session. The session level reports as session.
  3. Check pg_db_role_setting before editing a file. A per-role or per-database override makes a file change invisible for the sessions that matter.
  4. Use per-role statement_timeout and idle_in_transaction_session_timeout to bound workloads. It needs no application change and no restart, and it is the cheapest control in this course.
  5. Never treat a server-wide work_mem as a ceiling. It is user context, any session can raise it, and it is allocated per operation rather than per session.
  6. Reset at the level that set it. Resetting the wrong level reports success and changes nothing.

Cross-course references

  • Secrets, PKI & Certificate Management — Part XII (Secret management platforms) covers least-privilege policy design, and the per-role settings here are the database expression of the same idea.
  • Observability for Production Sysadmins — Part LIX (Database observability) covers exporting per-session settings so that the gap between the configured default and the effective value is visible.
  • Git, CI/CD & GitOps — Part LXXV (Drift) covers detecting configuration that lives outside the repository, which per-role settings in a catalogue very much are.

Quiz

Knowledge check · 6 questions

  1. Q1. pg_settings reports work_mem with source = 'user'. Which level supplied that value?

  2. Q2. An operator lowers work_mem in postgresql.conf during an incident and reloads. Some sessions continue using a much larger value. What is the most likely explanation?

  3. Q3. Which of these are true about per-role and per-database settings? Select all that apply.

  4. Q4. Setting work_mem in postgresql.conf establishes an upper bound on how much working memory any session may allocate.

  5. Q5. List the five levels a PostgreSQL setting can be attached to, from least specific to most specific.

  6. Q6. Explain the discrepancy and give the investigation order.

    A capacity review calculated worst-case working memory as max_connections of 200 multiplied by the configured work_mem of 8MB, giving 1.6GB, and concluded the 32GB host had ample headroom. Three weeks later the host was subject to an OOM kill during a nightly batch window. Investigation found that the batch job's sessions were each consuming several hundred megabytes. The configuration file still reads work_mem = 8MB and nobody has edited it.

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