โ† All posts
GUIDE August 26, 2026 ยท 4 min read

What your Postgres is waiting for

SR
Sentrinel Team
Product & engineering

Application monitoring tells you a request took 900 ms and that most of it was a query. It stops there, at the edge of the connection. What the database was doing for those 900 ms โ€” waiting on a lock held by whom, scanning which table, sitting behind which checkpoint โ€” lives on the other side, in views the application never sees.

Sentrinel's database monitoring goes and reads those views. It runs as a small collector next to the database, on a key that can send Postgres statistics and nothing else, and it answers the four questions that a slow query dashboard cannot.

Which queries own the machine

pg_stat_statements is the best-kept secret in Postgres: every distinct query shape, with total time, calls, rows, and buffer hits. The collector reads it and ranks queries by their share of total execution time, not by their average. A query that takes 2 ms but runs forty thousand times an hour is what is heating your CPU; sorted by average it would never make the first page.

Each shape carries its rows returned, cache hits versus disk reads, temp files written, and WAL generated โ€” the numbers that say why a query is expensive, not just that it is.

What it is waiting for, right now

Every second, the collector samples pg_stat_activity: every backend, its state, and its wait event. Over a minute that becomes a picture of where time goes โ€” Lock, IO, LWLock, ClientRead โ€” instead of a single "active connections" gauge.

Locks get special treatment. When a backend is waiting, pg_blocking_pids() says which backend it is waiting on, and the collector follows the chain: this query is blocked by that transaction, which is blocked by that idle-in-transaction session that opened at 09:14 and never committed. That chain, with query text at each link, is the thing you want at 09:31 and can never reconstruct afterwards.

The four ways a database stops

Most Postgres outages are one of four things, and none of them is a slow query:

Each is a small number that is easy to watch and easy to alert on: connection count against the maximum, cache hit ratio, idle-in-transaction age, replica lag in bytes. The collector reports all four every interval, and the dashboard's health page is those four numbers and their history.

Bloat, and the index you are missing

Less urgent, more expensive over time. Dead tuples per table, table size against what the live rows should need, indexes that are never scanned but are maintained on every write, and โ€” from the query statistics โ€” sequential scans on large tables that a missing index would turn into lookups. These are advisories, not alerts: things to fix on a quiet afternoon rather than during an incident.

What it does not send

Query text is collected in full by default, because a masked query is often useless for diagnosis. It can be masked: literals are stripped in the collector, before anything leaves the host, so the API never sees the values. It is off by default and one setting to turn on.

Explain plans and plan-change history are not collected yet. It is the next thing.

Running it

The collector is one binary, installed as a service and driven by a small CLI:

sentrinel-collector check        # connect, and report what it can see
sentrinel-collector start
sentrinel-collector status       # is it running, and what is it watching
sentrinel-collector logs -f

It needs a Database collector key โ€” issued from the dashboard, and the only kind that can reach the collector's endpoints. It lives on the database host, so it was built to be safe to lose there: a leaked one cannot post application telemetry, cannot read anything, and identifies itself by its prefix. Several instances on one machine, and one cluster with many databases, are both handled; the details are in the database guide.

Why a collector and not the plugin

The plugin lives in your application and sees connections from the outside. The statistics above are visible only from a session inside the database, with the right grants, and they are expensive enough to read that you want it done once per interval, not once per request. A separate process next to the database is the honest shape of the problem โ€” and it means the database can be monitored even when the application in front of it is the thing that is down.