Monitoring & Metrics Key Points
Monitoring a PostgreSQL cluster is not about collecting every number the system exposes, it is about knowing which few numbers actually predict user pain.
Search across all documentation pages
Monitoring a PostgreSQL cluster is not about collecting every number the system exposes, it is about knowing which few numbers actually predict user pain.
This page builds the mental model behind that filtering: what each class of signal represents, how the signals relate to each other, and how to reason from a symptom back to a root cause instead of staring at a wall of graphs.
Every PostgreSQL monitoring signal falls into one of a small number of categories: connections, locks, replication, storage/bloat, and query workload.
Connections describe how close the cluster is to max_connections, a hard ceiling past which new sessions are simply refused.
Locks and wait events describe whether backends are actively working or stuck waiting on something else, which is often the real cause behind a "slow" complaint.
Replication lag measures how far a standby or logical subscriber has fallen behind the primary, which matters for both read freshness and disaster-recovery posture.
Bloat signals, primarily dead tuple counts, describe whether autovacuum is keeping up with the write workload or slowly losing ground.
Query workload metrics, most visibly from pg_stat_statements, describe which queries consume the most cumulative database time rather than which single execution was slowest.
The unifying idea across all five categories is that PostgreSQL exposes this state directly through pg_stat_* system views, so monitoring tools are relaying data the database already tracks, not inventing new instrumentation.
A useful mental shortcut is to treat monitoring as answering one recurring question: is anyone waiting right now, and if so, on what resource.
Connections answer "can new work even start," locks and wait events answer "why is existing work stuck," and replication lag answers "how stale is the data a reader might see."
These signals do not exist independently, they form a causal chain that experienced operators read in order during an incident.
A spike in query latency often starts as increased lock contention, which shows up as backends accumulating in pg_stat_activity with a non-null wait_event.
If that contention persists, active connections pile up faster than they complete, pushing the cluster toward the max_connections ceiling and eventually causing new connections to be rejected outright.
Meanwhile, if the workload includes long-running transactions, autovacuum falls behind on the tables those transactions touch, and dead tuple counts climb, which degrades query plans further and can compound the original latency problem.
Replication lag typically follows rather than leads this chain, because a busy primary generates WAL faster than a standby can replay it, so lag is often a downstream symptom of the same write pressure causing lock contention.
-- One mechanism made concrete: wait_event is null while a backend
-- is actively executing, and non-null the moment it is blocked
-- waiting on a lock, I/O, or another backend.
SELECT pid, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE state = 'active'
AND pid <> pg_backend_pid();That query is the single most useful triage step during an incident, because a populated wait_event column tells you immediately whether the database is doing real work or stuck, which changes the entire investigation path.
A common reasoning error is watching only CPU utilization, since PostgreSQL workloads are frequently bound by lock waits or disk I/O rather than raw compute, so a healthy CPU graph can coexist with a cluster that is effectively frozen for users.
At scale, monitoring extends beyond the database process itself into the connection pooler, the host, and the alerting policy that turns metrics into pages.
A connection pooler like PgBouncer introduces its own saturation point, cl_waiting, that can rise while the underlying Postgres instance still has headroom, so pooler metrics need to be collected as a distinct signal, not inferred from database-side numbers alone.
Alert design is itself an architectural decision: alerting on every metric that deviates from baseline produces fatigue and trains responders to dismiss pages, while alerting only on symptoms users actually feel, like connection exhaustion or lag beyond an agreed budget, keeps signal-to-noise high.
Baselines matter more than absolute thresholds, because "80% CPU" means something different on a cluster that normally runs at 20% versus one that normally runs at 70%, so mature setups track week-over-week deltas alongside static ceilings.
Managed PostgreSQL platforms expose their own infrastructure dashboards, but those dashboards typically stop at the host and instance layer, leaving query-level and lock-level visibility as something the application team must still add.
Long-term metric retention enables a different class of question than real-time dashboards do, letting teams correlate a slow-degrading bloat trend with a deploy from three weeks earlier rather than only reacting to the moment an alert fires.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| postgres_exporter + Prometheus | Rich, standard pg_stat_* coverage | Requires self-managed scrape/alert stack | Kubernetes or VM-hosted clusters |
| Cloud-native dashboards (CloudWatch, Cloud Monitoring) | Zero setup, tied to managed instance | Weak on lock/query-level detail | RDS, Cloud SQL, and similar managed platforms |
pg_stat_statements ad hoc queries | Immediate, no extra infra | Manual, easy to forget during incidents | Quick triage and query-tuning sprints |
| Full APM (Datadog, New Relic) | Correlates app traces with SQL | Cost and another system to operate | Teams needing app-to-query correlation |
cl_waiting rising) while the Postgres side still shows plenty of free connections.Whether users are currently waiting on the database, and if so, which resource or lock is causing that wait.
Postgres workloads are frequently bound by lock contention or disk I/O, so CPU can appear low while sessions are effectively stuck.
A non-null wait_event tells you immediately that a backend is blocked rather than actively executing, pointing triage toward locks, I/O, or another backend.
No, a pooler like PgBouncer can show rising cl_waiting while the Postgres instance still has free backend capacity.
Cumulative impact better reflects real workload cost, since a frequent cheap query can consume more aggregate database time than a rare slow one.
Write pressure on the primary generating WAL faster than the standby or subscriber can replay it, not usually a raw network issue.
No, alerting on every deviation causes fatigue; alert policies should target symptoms users actually feel, like connection exhaustion or lag past an agreed budget.
The same percentage utilization means something different on a cluster that normally idles low versus one that normally runs hot, so relative deltas catch real anomalies better than static ceilings.
Not really, since rising dead tuple counts degrade query plans over time, so bloat is often an early leading indicator of a coming performance problem.
They cover host and instance metrics well, but query-level and lock-level visibility usually still needs to be added by the application team.
It lets teams correlate a slow-building trend, like creeping bloat or connection growth, with a deploy or schema change from weeks earlier.
Teams either drown in noise from too many low-value alerts or miss the handful of signals that actually precede an incident.
Stack versions: This page was written for PostgreSQL 18.4 (stable major 18, maintenance line 17) and PgBouncer 1.x.
Reviewed by Chris St. John·Last updated Jul 15, 2026