Module A-12·40 min read

The difference between junior and senior is not SQL knowledge — it is having the right runbooks before the incident.

Module 12 — Production Operations: Monitoring, Migration, and the Runbooks That Matter

What this module covers: The difference between a junior and a senior database engineer is not SQL knowledge — it is having the right runbooks before the incident, the right monitoring before the alert, and the right migration plan before the deployment. This module covers the operational stack: what to measure and how, connection pooling with PgBouncer, backup and restore, zero-downtime migration patterns, and the five runbooks that every team running Postgres in production needs to have written down before they need them.


The Monitoring Stack

You cannot operate what you cannot observe. Postgres exposes an exceptional amount of internal state through pg_stat_* views. The challenge is not access — it is knowing which metrics to collect, what thresholds to alert on, and what to do when they fire.

The Essential Views

sql

idle in transaction connections are the most dangerous. They hold open MVCC snapshots, pin OldestXmin, and block autovacuum from reclaiming dead tuples (Module 2 and 4). Any connection idle in transaction for more than 30 seconds deserves investigation.

sql
sql
sql
sql
sql

pg_stat_statements: Query Performance Over Time

sql
sql

Alerting Thresholds

MetricWarningCriticalAction
Connections used (% of max_connections)> 70%> 85%Check for connection leaks; increase PgBouncer pool
idle in transaction connections> 5> 20Kill offending connections; fix application code
Replication lag (bytes)> 100MB> 500MBInvestigate standby I/O; reduce write load
Dead tuple % on any table> 20%> 40%Run manual VACUUM; tune autovacuum for that table
XID age on any table> 1B> 1.5BRun VACUUM FREEZE immediately
Cache hit ratio< 99%< 95%Increase shared_buffers; optimize query working set
pg_wal directory size> 60% of partition> 80%Check inactive replication slots; increase disk
Long-running queries> 30s> 5minIdentify and kill; add timeout: statement_timeout
Lock wait time> 5s> 30sFind blocking query; investigate lock contention
ini

idle_in_transaction_session_timeout is one of the most important production settings that most teams don't have set. It automatically terminates connections that have been idle-in-transaction for too long — eliminating the entire class of "someone left a transaction open in their REPL and the table hasn't vacuumed in 6 hours" incidents.


Connection Pooling: PgBouncer

Every Postgres connection is a full OS process (Module 0). Above a few hundred connections, fork overhead and memory pressure become significant. PgBouncer is the standard connection pooler — a lightweight proxy that multiplexes many application connections onto a smaller number of Postgres connections.

PgBouncer Pooling Modes

Session mode:

  • One Postgres connection is assigned to an application connection for the entire session
  • Prepared statements, advisory locks, and SET parameters work correctly
  • Pool size = max simultaneous sessions ≈ similar to no pooler for connection count
  • Use when: you need prepared statements or session-level state

Transaction mode:

  • A Postgres connection is held only during a transaction; returned to the pool after COMMIT/ROLLBACK
  • One Postgres connection can serve many sequential application transactions
  • Breaks: prepared statements (not persisted across transactions), advisory locks, SET parameters, LISTEN/NOTIFY
  • Use when: high-throughput OLTP with short transactions and no prepared statements
  • Most PgBouncer deployments use this mode

Statement mode:

  • A Postgres connection is held only for a single statement
  • Breaks: multi-statement transactions (each statement gets its own connection → no transaction atomicity)
  • Rarely used in practice
ini

Sizing the Pool

The optimal number of Postgres connections is not "as many as possible" — it is the number that saturates your hardware without thrashing.

A well-known formula (from PgBouncer's own documentation):

optimal_pool_size ≈ (num_cores × 2) + num_effective_spindles

For a 16-core server with NVMe (no spindles):

optimal ≈ 16 × 2 + 1 = 33 connections

More connections than this means CPU context switching and memory pressure outweigh the benefit of additional concurrency. PgBouncer's default_pool_size = 30–50 for a modern server is a reasonable starting point.

sql

cl_waiting > 0 consistently means your pool is undersized for the workload. Either increase default_pool_size (requires more Postgres connections) or reduce application connection count.


Backup Strategies

pg_dump: Logical Backup

bash

Advantages: portable across Postgres versions, table-selective, works through logical replication boundaries.
Disadvantages: slow for large databases (must serialize every row), creates significant I/O load on the source, cannot be used for PITR.

pg_basebackup: Physical Backup

bash

Advantages: fast (parallel I/O), complete cluster copy, can be used as base for PITR, can be used directly as a standby.
Disadvantages: not portable across major versions, full cluster only (not table-selective), requires a replication connection.

pgBackRest: Production Backup Tool

For production systems, pgBackRest is the standard. It handles:

  • Incremental and differential backups (only changed blocks since last backup)
  • Parallel backup and restore
  • WAL archiving integrated with backup
  • Backup verification
  • Remote backup to S3, Azure, GCS
ini
bash

Backup Testing

A backup you have never tested is not a backup — it is a false sense of security. Schedule quarterly restore drills:

bash

Zero-Downtime Migration Checklist

Every schema change on a live production database must be evaluated against this checklist.

Operations That Lock (Avoid Without a Plan)

sql

Safe Operations (No Significant Lock)

sql

The Migration Sequence

Adding a NOT NULL column with a default (PG 10 and earlier):

sql

Renaming a column (zero downtime):

sql

Adding a new index:

sql

The Five Runbooks

Every production Postgres team needs these written down before they need them. The worst time to write a runbook is during an incident.

Runbook 1: Table Bloat Emergency

Symptoms: table size growing despite stable row count; autovacuum running continuously without catching up; queries slowing down on a table.

Diagnosis:

sql

Response:

sql

Runbook 2: Replication Lag Spike

Symptoms: pg_stat_replication.replay_lag climbing; standby reads returning stale data; alerting on replication bytes lag.

Diagnosis:

sql

Response:

sql

Runbook 3: XID Wraparound Warning

Symptoms: PostgreSQL log shows WARNING: database "X" must be vacuumed within N transactions; monitoring alert on age(relfrozenxid).

Diagnosis:

sql

Response:

sql

Runbook 4: Connection Exhaustion

Symptoms: application errors FATAL: remaining connection slots are reserved for non-replication superuser connections; new connections rejected; pg_stat_activity shows connections at or near max_connections.

Diagnosis:

sql

Response:

sql

Runbook 5: Slow Query During Incident

Symptoms: specific query suddenly slow; pg_stat_activity shows many backends waiting; p99 latency spiking.

Diagnosis:

sql

Response:

sql

auto_explain: Automatic Plan Logging

For queries that are intermittently slow, auto_explain logs the execution plan automatically when execution time exceeds a threshold.

ini

This is invaluable for diagnosing intermittent slowdowns — you get the actual plan at the time of slowness without having to reproduce it.


The Production Incident: Connection Exhaustion Cascade

Context: A blockchain indexer with 500 max_connections behind PgBouncer in transaction mode.

What happened:

A deployment introduced a bug: under certain error conditions, the application's database connection was not returned to the pool — it was abandoned while holding an open transaction. The transaction stayed idle in transaction.

At normal load: the leak rate was slow enough that connections accumulated over hours without triggering alerts.

At 2:00 AM, a spike in blockchain traffic increased error rate. The leak accelerated. Within 20 minutes:

  • PgBouncer's cl_waiting counter climbed (application connections queuing for a server connection)
  • Application timeouts increased (requests waiting for a connection from the pool)
  • Application retried timed-out requests (each retry needed its own connection)
  • The retry storm caused even more connections to be consumed
  • PgBouncer ran out of server connections; applications received connection refused
  • Applications retried again — the storm intensified

Peak: 12,000 application connections to PgBouncer, 500 Postgres connections all idle in transaction, zero connections available for legitimate queries.

Resolution sequence:

sql

The root fix: idle_in_transaction_session_timeout = '30s' was added to postgresql.conf. Any connection that stays idle-in-transaction for more than 30 seconds is now automatically terminated by Postgres. The connection leak bug was fixed in the application, but the timeout ensures any future bug of the same class is self-healing.

Post-incident addition: a PgBouncer alert on cl_waiting > 100 was added — the waiting queue was the earliest visible signal, 15 minutes before the outage became critical.


Summary

AreaKey Takeaway
Monitoringpg_stat_activity, pg_stat_statements, pg_stat_user_tables, pg_stat_replication — know these cold
idle_in_transaction_session_timeoutSet it. 30–60 seconds. Prevents an entire class of bloat and connection leak incidents
statement_timeoutSet it (300s or less). Prevents runaway queries from holding locks or consuming resources indefinitely
PgBouncer modeTransaction mode for OLTP; session mode only if you need prepared statements
Pool sizing(num_cores × 2) + 1 is a reasonable starting point for Postgres connections
Backuppg_dump for portability; pg_basebackup + WAL archive for PITR; pgBackRest for production
Backup testingRestore drills quarterly — an untested backup is not a backup
Schema migrationsCREATE INDEX CONCURRENTLY, batched backfills, NOT VALID + VALIDATE pattern
RunbooksWrite them before you need them. The worst time to learn these queries is during an incident

Course Complete

You have now covered the full operational stack of PostgreSQL — from the 8KB page on disk to the replication slot across availability zones. The mental model this course set out to build in Module 0 is now complete.

The path from here:

Practice: run the pageinspect, pgstattuple, and pg_stat_* queries against a real database. The numbers will be different from the examples — that is the point. Learn to read your specific system.

Operationalize: implement the alerting thresholds, write the runbooks, add idle_in_transaction_session_timeout to every production instance you run.

Go deeper: the Postgres source code is readable C. The storage manager, the executor, the planner — all of it is there. backend/storage/buffer/, backend/executor/, backend/optimizer/ are where the concepts in this course live in code.

The database does not lie. Every behavior has a cause in the mechanics described here. When something unexpected happens in production, the answer is in pg_stat_*, in WAL, in the heap pages — you now have the model to find it.


Knowledge Check

A Senior Software Engineer is designing a new microservice that heavily relies on prepared statements for performance and uses session-level SET parameters for tenant isolation. They are considering deploying PgBouncer to manage database connections. Which PgBouncer pooling mode should they choose, and what are the implications?


A team needs to add a new NOT NULL column with a default value to a large, high-traffic 'transactions' table on a PostgreSQL 10 instance without causing any application downtime or significant performance degradation. Which sequence of operations is the most appropriate for a zero-downtime migration?


During a peak traffic period, a PostgreSQL database experiences a sudden increase in n_dead_tup on a critical table, slow autovacuum progress, and queries on that table start timing out. Upon inspecting pg_stat_activity, a significant number of 'idle in transaction' connections are observed, some active for several minutes. Which of the following is the most immediate and effective set of actions to mitigate the incident and prevent recurrence, based on the provided runbooks and settings?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.