Skip to main content
RunBook Academy

PostgreSQLI · Architecture and the Process ModelArchitecture

The operator's first instruments

Intermediate⏱ ~30 min🧪 Lab requiredpsql

What you'll learn

  • Read pg_settings including source, context and pending_restart
  • Query pg_stat_activity for the four states an operator acts on differently
  • Use pg_stat_io to attribute I/O to a backend type and a context
  • Assemble a first-five-minutes triage sequence and know what each query rules out

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.

PostgreSQL describes itself. Its configuration, its running sessions, its locks, its I/O and its table statistics are all queryable through ordinary SQL against catalogue views, which means that almost every question asked in the opening minutes of an incident has an answer available from a psql prompt.

This lesson covers the three views that answer the most questions per keystroke. The rest of the course adds more — pg_locks in Part IX, pg_stat_replication in Part XIV, pg_stat_statements in Part XVI — but these three come first because they are where you start.

pg_settings: what is this server configured to do

SHOW work_mem gives you a value. pg_settings gives you the value plus everything you need to reason about it: where it came from, whether you can change it now, and whether a change you already made has actually taken effect.

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

Four columns carry most of the value.

source distinguishes a deliberate choice from a default. Filtering to source <> 'default' is the fastest way to see what somebody actually configured on a server you have just inherited, and it is a much shorter list than the full parameter set.

context is the change-planning column from earlier in this part: postmaster needs a restart, sighup needs a reload, superuser and user can be set in a session.

pending_restart is the honesty column. It is t when the configuration file holds a value that differs from the one the server is running, for a parameter that needs a restart. This is the difference between a change you made and a change that is in effect, and it is the column that settles the question “did that change apply?”

sourcefile and sourceline name the file and line the value came from, which matters once postgresql.auto.conf and include files are in play. Part III builds on this.

# Has anything been edited but not yet restarted into effect?
psql -U postgres -c \
  "SELECT name, setting, sourcefile, sourceline
     FROM pg_settings WHERE pending_restart"

# Where did this specific value come from?
psql -U postgres -c \
  "SELECT name, setting, source, sourcefile, sourceline
     FROM pg_settings WHERE name = 'shared_buffers'"

pg_stat_activity: what is it doing right now

One row per backend, including the background processes. The columns that decide your next action are backend_type, state, wait_event_type and the two age calculations you derive from the timestamps.

The single most useful query in this course:

psql -U postgres -c \
  "SELECT pid,
          usename,
          application_name,
          client_addr,
          state,
          wait_event_type || ':' || wait_event AS waiting_on,
          now() - xact_start AS xact_age,
          now() - state_change AS in_state,
          left(query, 60) AS query
     FROM pg_stat_activity
    WHERE backend_type = 'client backend'
      AND pid <> pg_backend_pid()
    ORDER BY xact_start NULLS LAST"

Two details in that query are deliberate. Filtering to client backend removes the background processes, which are permanently in an Activity wait and would otherwise dominate the output — the reason was covered in the background-processes lesson. Excluding pg_backend_pid() removes your own session, which is always active and always running the query you are looking at.

The four states, and why they need different responses

stateWhat it meansThe right response
activeExecuting a query nowLook at wait_event: working, or blocked?
idleConnected, no transaction openHolds a process and its memory; harmless to the database
idle in transactionTransaction open, doing nothingActively harmful. Holds locks and blocks cleanup
idle in transaction (aborted)Same, after an errorSame harm; the client has not sent a rollback

The distinction between idle and idle in transaction is the most consequential thing on this page. An idle session is merely a process. An idle in transaction session is holding a transaction open, which means it is holding whatever locks it took, and — as Part VII and Part VIII will show — it is preventing VACUUM from reclaiming dead rows anywhere in the cluster, because a row version that transaction might still need cannot be removed.

A single connection left idle in transaction overnight is a routine cause of a table growing without bound while its row count stays flat.

# The query to run before you conclude anything about bloat or vacuum.
psql -U postgres -c \
  "SELECT pid, usename, application_name, state,
          now() - xact_start AS xact_age, left(query, 80) AS last_query
     FROM pg_stat_activity
    WHERE state LIKE 'idle in transaction%'
    ORDER BY xact_start"

wait_event_type and wait_event turn active from one state into many. A backend that is active might be computing, waiting on I/O, waiting for a lock, or waiting on a client that has stopped reading. Those are four different incidents. Part XVI covers wait events systematically; the shape to remember now is that active alone tells you almost nothing.

pg_stat_io: where is the I/O going

Introduced in PostgreSQL 16 and extended in 18, pg_stat_io breaks I/O down by which kind of process did it, what it was operating on, and why.

Read-only / SafeI/O attributed to process type and purpose
$ psql -U postgres -c 'SELECT backend_type, object, context, reads, writes, extends, hits FROM pg_stat_io WHERE reads > 0 OR writes > 0 ORDER BY backend_type'
    backend_type     |  object  | context  | reads | writes | extends | hits
---------------------+----------+----------+-------+--------+---------+-------
autovacuum launcher | relation | normal   |     1 |      0 |         |     0
autovacuum worker   | wal      | normal   |       |     11 |         |
autovacuum worker   | relation | normal   |   137 |      0 |       9 | 11248
autovacuum worker   | relation | vacuum   |    40 |      0 |       0 |   126
checkpointer        | relation | normal   |       |    389 |         |
checkpointer        | wal      | normal   |     0 |      2 |         |
client backend      | relation | normal   |   334 |      0 |       0 | 15381
client backend      | relation | bulkread |   215 |      0 |         |   345
client backend      | wal      | normal   |     0 |     38 |         |
standalone backend  | relation | normal   |   479 |   1038 |     598 | 92643
(10 rows)

The question this view answers that no host-level tool can: which part of PostgreSQL is generating this I/O, and for what purpose. iostat shows a busy device. This shows that the writes are the checkpointer rather than the application, or that the reads are an autovacuum worker rather than a query.

context is the column that repays study:

  • normal — ordinary buffered access through the shared pool.
  • vacuum — work done by vacuum, using its own restricted buffer ring.
  • bulkread — large sequential reads, also using a restricted ring so they cannot evict the working set.
  • bulkwrite — bulk loading operations such as COPY.

A low hit rate in bulkread or vacuum context is the ring buffer working correctly, not a cache problem. Reading a pooled hit ratio without this breakdown is how the wrong conclusion gets reached, which was the subject of the buffer-pool lesson.

In PostgreSQL 18 this view gained read_bytes, write_bytes and extend_bytes, replacing the single op_bytes column that 16 and 17 carried. A monitoring query written against 16 or 17 that selects op_bytes will fail on 18 — the same class of post-upgrade breakage as the pg_stat_bgwriter column moves, and worth checking at the same time.

A first-five-minutes sequence

The value of a fixed sequence is that each step rules something out, so you are narrowing rather than wandering.

# 1. Is the server up and accepting connections at all?
pg_isready

# 2. Is it in recovery? A standby, or still replaying after a crash.
psql -U postgres -tAc 'SELECT pg_is_in_recovery()'

# 3. How many sessions, and in what states?
psql -U postgres -c \
  "SELECT state, count(*) FROM pg_stat_activity
    WHERE backend_type = 'client backend' GROUP BY state ORDER BY count(*) DESC"

# 4. Is anything blocked, and how long has the oldest transaction been open?
psql -U postgres -c \
  "SELECT pid, state, wait_event_type, wait_event,
          now() - xact_start AS xact_age, left(query, 60) AS query
     FROM pg_stat_activity
    WHERE backend_type = 'client backend' AND xact_start IS NOT NULL
    ORDER BY xact_start LIMIT 10"

# 5. Where is the disk going?
psql -U postgres -c \
  "SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size
     FROM pg_database ORDER BY pg_database_size(datname) DESC"

Step 2 is the one people omit and regret. pg_is_in_recovery() returning true means this server is a standby or is still replaying, and a great many symptoms — writes being refused, data appearing stale, queries being cancelled — have that single explanation. Establishing it in one query at the start saves a long detour.

Production discipline

  1. Check pending_restart after every configuration change. It is the one column that distinguishes an intended change from an applied one.
  2. Filter pg_stat_activity to client backend before counting anything. Background processes are permanently in an Activity wait and will distort any count that includes them.
  3. Treat idle in transaction as a finding, never as noise. It holds locks and blocks cleanup cluster-wide, and it explains symptoms that appear entirely unrelated.
  4. Capture application_name, client_addr and transaction age before terminating anything. Terminating removes the evidence of which code path is responsible.
  5. Read statistics as two samples, not one number. They are cumulative since stats_reset, and a crash resets them to zero.
  6. Run pg_is_in_recovery() early. It explains a large family of symptoms in a single query.

Cross-course references

  • Observability for Production Sysadmins — Part LIX (Database observability) covers turning these views into exported metrics, and Part XII (PromQL foundations) covers computing rates from cumulative counters, which is precisely what these views require.
  • Linux for Production Sysadmins — Part XLI (Disk performance) covers the host-level I/O picture that pg_stat_io attributes to a cause.
  • Git, CI/CD & GitOps — Part CVI (Change management) covers recording a configuration change so that pending_restart has something to be reconciled against.

Quiz

Knowledge check · 6 questions

  1. Q1. An operator raised shared_buffers in postgresql.conf four hours ago and reloaded the configuration. Which single query establishes whether the new value is actually in effect?

  2. Q2. A table has grown steadily for a week while its row count has stayed flat, and autovacuum is enabled and running. Which pg_stat_activity finding would best explain this?

  3. Q3. Which of these are true about the PostgreSQL cumulative statistics views? Select all that apply.

  4. Q4. A low buffer hit ratio reported under the bulkread context in pg_stat_io is expected behaviour rather than a sign that shared_buffers is too small.

  5. Q5. Name two filters that should be applied to pg_stat_activity before counting sessions, and explain what each one removes.

  6. Q6. Run the triage and state what the evidence supports.

    At 03:00 an application reports database timeouts. The database process is running and pg_isready reports the server is accepting connections. pg_stat_activity grouped by state shows 4 active, 11 idle, and 87 idle in transaction. The oldest transaction started at 22:14 the previous evening. pg_stat_io shows autovacuum worker rows with substantial reads in vacuum context. Host CPU is 20% and disk utilisation is moderate. The application team reports that a deployment went out at 22:10.

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