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.
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.
Summary
- Core Idea: Monitoring translates internal database state into a small set of signals that predict whether users are waiting, and why.
- Why It Matters: Without a mental model, teams either drown in low-value alerts or miss the handful of signals that actually precede an incident.
- Key Concepts: saturation, wait events, replication lag, cache hit ratio, query workload, alert fatigue.
- When to Use: Building first dashboards, triaging "the database is slow" tickets, and deciding what a new cluster's alerting policy should cover.
- Limitations / Trade-offs: More metrics are not automatically more insight, and over-alerting trains on-call engineers to ignore pages.
- Related Topics: query workload analysis, host-level resource metrics, connection pooling, replication topology.
Foundations
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."
Mechanics & Interactions
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.
Advanced Considerations & Applications
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 |
Common Misconceptions
- "High CPU means the database is overloaded." - PostgreSQL is frequently lock- or I/O-bound, so CPU can look idle while sessions queue up behind a blocking transaction.
- "A healthy connection count means no capacity problem." - a pooler can be saturated (
cl_waitingrising) while the Postgres side still shows plenty of free connections. - "pg_stat_statements shows the slowest query." - it ranks queries by cumulative impact, so a query called a million times at 5ms can outrank one slow 5-second outlier.
- "Replication lag is always a network problem." - lag most often reflects write pressure on the primary generating WAL faster than the standby can replay it.
- "More dashboards mean better observability." - a wall of unprioritized graphs is often less useful than five signals mapped to the symptoms users actually report.
- "Managed Postgres monitoring is complete out of the box." - provider dashboards typically cover host and instance metrics, not lock waits or query-level workload.
FAQs
What is the single most important question PostgreSQL monitoring should answer?
Whether users are currently waiting on the database, and if so, which resource or lock is causing that wait.
Why is CPU utilization a misleading primary metric for PostgreSQL health?
Postgres workloads are frequently bound by lock contention or disk I/O, so CPU can appear low while sessions are effectively stuck.
How does `wait_event` in `pg_stat_activity` help during an incident?
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.
Does a low connection count guarantee the connection pooler is healthy too?
No, a pooler like PgBouncer can show rising cl_waiting while the Postgres instance still has free backend capacity.
Why does `pg_stat_statements` rank by total time rather than by the single slowest execution?
Cumulative impact better reflects real workload cost, since a frequent cheap query can consume more aggregate database time than a rare slow one.
What causes replication lag most commonly?
Write pressure on the primary generating WAL faster than the standby or subscriber can replay it, not usually a raw network issue.
Should every metric that deviates from normal trigger an alert?
No, alerting on every deviation causes fatigue; alert policies should target symptoms users actually feel, like connection exhaustion or lag past an agreed budget.
Why do baselines matter more than fixed thresholds?
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.
Is bloat monitoring separate from performance monitoring?
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.
Do managed PostgreSQL platforms remove the need for custom monitoring?
They cover host and instance metrics well, but query-level and lock-level visibility usually still needs to be added by the application team.
Why is long-term metric retention valuable beyond real-time alerting?
It lets teams correlate a slow-building trend, like creeping bloat or connection growth, with a deploy or schema change from weeks earlier.
What is the risk of collecting metrics without a mental model of how they relate?
Teams either drown in noise from too many low-value alerts or miss the handful of signals that actually precede an incident.
Related
- Monitoring Basics - the recipe-level starting point for the signals introduced here.
- pg_stat_activity & Locks - a closer look at the wait-event mechanics described above.
- pg_stat_statements - the query-workload signal in depth.
- SLO Dashboards - turning these signals into user-facing budgets.
- postgres_exporter & Grafana - the collection stack referenced in the comparison table.
- Logging & Audit Trails Explained - the complementary forensic signal alongside metrics.
Stack versions: This page was written for PostgreSQL 18.4 (stable major 18, maintenance line 17) and PgBouncer 1.x.