Architecture and operations
David Sterling  

PostgreSQL Production Monitoring: 7 Metrics That Matter

Monitoring is the backbone of every production PostgreSQL deployment. Whether you run Patroni-managed high availability on your own hardware or Amazon Aurora in the cloud, you cannot fix what you cannot see. Yet most monitoring setups I audit collect hundreds of metrics and alert on almost none of them — or worse, they page a human for every blip and train everyone to ignore the alerts.

This is the monitoring layer I recommend to Houston teams running Postgres for energy, healthcare, and logistics workloads, where a five-minute outage can stall a pipeline control system or a hospital scheduling queue. Start with these seven metrics, the configuration that surfaces them, and the alerts worth waking up for.

1. Enable pg_stat_statements first

The single highest-ROI change you can make is turning on pg_stat_statements. It records per-query timing, row counts, and I/O statistics with no external agent and no application changes. Add this to postgresql.conf:

# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.max = 10000
pg_stat_statements.track = all
track_io_timing = on

Restart Postgres, then create the extension:

CREATE EXTENSION pg_stat_statements;

Within a day you will have the answer to the most common question in operations: “which queries are actually slow?” Stop guessing and look at the data:

SELECT queryid, calls,
       round(mean_exec_time::numeric, 2) AS avg_ms,
       round(max_exec_time::numeric, 2) AS max_ms,
       rows, shared_blks_read
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

2. Watch wait events, not just query time

Query time is a symptom; wait events are the diagnosis. A query taking 200 ms because it is blocked on a lock is a different problem from one doing 50,000 random reads. pg_stat_activity shows what every backend is waiting on right now:

SELECT pid, usename, state,
       wait_event_type, wait_event,
       now() - query_start AS query_age
FROM pg_stat_activity
WHERE state <> 'idle'
  AND wait_event_type IS NOT NULL
ORDER BY query_age DESC;

If you see Client:ClientRead everywhere, your problem is application-side. If you see IO:DataFileRead, it is a disk or cache problem. If you see Lock:transactionid, you have a blocking chain — find the oldest backend and investigate before killing anything.

3. Replication lag is your canary

If you run high availability with Patroni or repmgr — or Aurora replicas — replication lag is the metric that determines whether failover serves fresh data or stale data. Under 30 seconds is fine for most applications; sustained lag above 60 seconds means the standby cannot keep up, and you should treat failover as risky.

SELECT client_addr, state,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)
         AS replay_lag_bytes
FROM pg_stat_replication;

Alert on the trend, not the instant: a two-second lag spike during a nightly batch job is noise; a lag that climbs steadily for ten minutes is a page.

4. Connection saturation comes before the crash

Postgres handles connection exhaustion badly: the database stays up while every new connection waits, and the application times out. Alert before you hit max_connections. With prometheus-community’s postgres_exporter, a rule like this catches it:

groups:
  - name: postgres
    rules:
      - alert: PostgresConnectionsHigh
        expr: pg_stat_database_numbackends / pg_settings_max_connections > 0.8
        for: 10m
        labels:
          severity: warning
      - alert: PostgresReplicationLag
        expr: pg_replication_lag_seconds > 60
        for: 5m
        labels:
          severity: page

5. Bloat and vacuum: the silent creep

Autovacuum handles routine cleanup, but it does not handle everything. Watch n_dead_tup growth and last_autovacuum in pg_stat_user_tables. When a heavily updated table’s dead-tuple count climbs for days, you have a vacuum backlog — and bloat that will surface as sudden I/O spikes during a Houston summer when the disks throttle. That one is more common than you think.

6. EXPLAIN is the second half of monitoring

When a slow query surfaces, do not guess — EXPLAIN. AWS recently published a solid walkthrough of using EXPLAIN plans to improve query performance in Amazon Aurora DSQL. Two rules from that guidance apply everywhere: run EXPLAIN (ANALYZE, BUFFERS) on realistic data volumes, and remember that a plan tuned for a 1 GB table will not hold at 100 GB. Also track engine versions: AWS’s guidance on when new open-source engine versions land on RDS and Aurora is worth following so your monitoring baselines do not silently drift across upgrades.

7. Capacity planning falls out of good monitoring

OpenAI’s engineering team recently shared how PostgreSQL scales to power 800 million ChatGPT users. The headline is fun, but the operations lesson is boring and important: they measured — query latency, connection churn, storage growth — and let the data drive scaling decisions. Your capacity plan should work the same way. Chart storage growth, forecast six months out, and provision before the chart crosses the line. That is the whole trick.

Start small, standardize, and write the runbook

You do not need fifteen tools. The 2026 monitoring landscape has no shortage of options — pgMonitor, pgwatch2, Percona Monitoring and Management, postgres_exporter with Grafana — but the tool matters less than the thresholds and the runbook behind them. Pick one stack, wire up the queries above, set five alerts, and document what to do when each one fires. In Houston, where the power grid and the weather do not care about your maintenance window, that runbook is what keeps a 2 a.m. page from becoming a 6 a.m. incident post-mortem.

Leave A Comment