{"id":631,"date":"2026-08-30T13:00:47","date_gmt":"2026-08-30T13:00:47","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/pg-statviz-1-2-postgres-monitoring-without-the-stack\/"},"modified":"2026-08-30T13:00:47","modified_gmt":"2026-08-30T13:00:47","slug":"pg-statviz-1-2-postgres-monitoring-without-the-stack","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/pg-statviz-1-2-postgres-monitoring-without-the-stack\/","title":{"rendered":"Postgres Monitoring Without the Stack: pg_statviz 1.2 in Practice"},"content":{"rendered":"<p>Okay, this is going to sound cheap, because it is: my entire Postgres monitoring stack costs zero dollars and runs off a systemd timer. No Datadog agent, no Grafana Cloud trial that quietly becomes a credit card charge, no agent eating 300MB of RAM on a 4GB VPS to tell me the database is, in fact, up. I&rsquo;ve been running <a href=\"https:\/\/github.com\/vyruss\/pg_statviz\" rel=\"nofollow noopener\" target=\"_blank\">pg_statviz<\/a> for this, and version 1.2 just landed with PostgreSQL 19 support and a genuinely useful new trick, so it&rsquo;s a good moment to write up the setup.<\/p>\n<p>If you&rsquo;ve never heard of it: pg_statviz is a minimalist pair of things, a tiny Postgres extension that snapshots the database&rsquo;s own internal statistics into a few tables, and a Python utility that turns those snapshots into time series charts. That&rsquo;s the whole product. The <a href=\"https:\/\/www.postgresql.org\/about\/news\/pg_statviz-12-released-with-postgresql-19-support-and-new-features-3369\/\" rel=\"nofollow noopener\" target=\"_blank\">1.2 announcement on postgresql.org<\/a> covers the new bits, and I&rsquo;ll get to my favorite one, but first I want to make the case for this whole category of tool, because I think most small teams are monitoring their databases wrong.<\/p>\n<h2 id=\"the-problem-with-most-postgresql-monitoring-tools\">The problem with most postgresql monitoring tools<\/h2>\n<p>The standard advice for Postgres observability assumes you have a platform team. Install an exporter, run Prometheus, run Grafana, maintain dashboards, configure alerting, and now you&rsquo;re maintaining five services to watch one. The SaaS route trades that maintenance for money and a data egress question you&rsquo;ll answer awkwardly in a compliance review someday.<\/p>\n<p>Here&rsquo;s what I actually need for the client databases I look after: when something feels slow on Thursday, I want to know what changed since Monday. That&rsquo;s it. That&rsquo;s 90% of real-world database investigation on small systems. It&rsquo;s a question about history, and Postgres, out of the box, barely keeps any. The stats collector&rsquo;s cumulative counters tell you totals since the last reset, which is like asking how your car is running and being told the odometer reading.<\/p>\n<p>Postgres 19 actually acknowledges this gap: the stats system keeps getting richer (more on <code>wal_fpi_bytes<\/code> below), but the history problem remains yours to solve. Snapshots solve it. pg_statviz just takes them for you, inside the database, with no external infrastructure.<\/p>\n<h2 id=\"setup-in-four-commands\">Setup in four commands<\/h2>\n<p>The extension is in PGDG repos, so on Debian-flavored systems:<\/p>\n<pre><code class=\"language-bash\">sudo apt install postgresql-17-statviz\nsudo -u postgres psql -c &quot;CREATE EXTENSION pg_statviz;&quot;\npipx install pg_statviz\nsudo -u postgres psql -c &quot;SELECT pgstatviz.snapshot();&quot;\n<\/code><\/pre>\n<p>That last command takes one snapshot. The tables live in the <code>pgstatviz<\/code> schema and storage is deliberately light, so taking snapshots every 15 minutes costs you approximately nothing.<\/p>\n<p>For scheduling, the README suggests cron, but I run it from a systemd timer for the same reasons <a href=\"https:\/\/abrarqasim.com\/blog\/systemd-timers-the-cron-job-i-stopped-trusting\/\" rel=\"noopener\">I moved my cron jobs to timers across the board<\/a>: logged output in journald, no silent mail-to-nowhere failures, and <code>systemctl list-timers<\/code> shows me the next run at a glance.<\/p>\n<pre><code class=\"language-ini\"># \/etc\/systemd\/system\/pgstatviz-snapshot.service\n[Unit]\nDescription=pg_statviz snapshot\n\n[Service]\nType=oneshot\nUser=postgres\nExecStart=\/usr\/bin\/psql -c &quot;SELECT pgstatviz.snapshot();&quot;\n\n# \/etc\/systemd\/system\/pgstatviz-snapshot.timer\n[Unit]\nDescription=pg_statviz snapshot every 15 minutes\n\n[Timer]\nOnCalendar=*:00\/15\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n<\/code><\/pre>\n<p>When you want charts, the Python side pulls the snapshots and renders them:<\/p>\n<pre><code class=\"language-bash\">pg_statviz --host localhost -U postgres -d myapp analyze\n<\/code><\/pre>\n<p>You get PNGs of WAL generation, buffer activity, connection counts, transaction rates, and so on, over whatever window your snapshots cover. It isn&rsquo;t pretty in the Grafana sense. It answers questions, which I&rsquo;ve come to prefer.<\/p>\n<h2 id=\"what-12-adds-and-why-the-locks-module-is-the-headline\">What 1.2 adds, and why the locks module is the headline<\/h2>\n<p>The PG19 support is table stakes (it&rsquo;s been tested across the whole 13-to-19 range, which is a nice compatibility promise for a small project). Two additions stand out.<\/p>\n<p>First, WAL visibility got sharper. The extension now captures the new <code>wal_fpi_bytes<\/code> counter from <code>pg_stat_wal<\/code> on 19, so you can finally see how much of your WAL volume is full-page images rather than actual change records. If your WAL spikes after every checkpoint and you&rsquo;ve never known why, that&rsquo;s the counter that explains it, and it&rsquo;s the kind of thing that turns &ldquo;storage bills feel high&rdquo; into a specific tuning conversation about checkpoint spacing.<\/p>\n<p>Second, and this is the one I&rsquo;ve wanted for years: a blocking locks analysis module. Each snapshot now records how many sessions were blocked and blocking, broken down by lock type. Crucially, detection is built on <code>pg_blocking_pids()<\/code>, so it counts soft blocks too, sessions waiting in the lock queue behind another waiter, not just direct hard conflicts. Lock contention is the classic invisible problem: it never shows up when you go looking, because looking means running a query at 2pm and the pileup happened at 9:15am. Having it snapshotted every 15 minutes means Thursday-me can see exactly which morning the trouble started and what kind of lock it was.<\/p>\n<p>The announcement also mentions that sustained blocking can never be reported as healthy thanks to a deterministic severity floor on the module&rsquo;s verdicts, which is a small design decision I appreciate. Monitoring tools that grade on a curve train you to ignore them.<\/p>\n<h2 id=\"where-it-fits-next-to-pg_stat_statements\">Where it fits next to pg_stat_statements<\/h2>\n<p>To be clear about scope: pg_statviz tells you what the server was doing over time. It won&rsquo;t tell you which query is responsible. For that you still want <code>pg_stat_statements<\/code>, and the two compose nicely: statviz narrows down when things went sideways and in which subsystem, then <a href=\"https:\/\/abrarqasim.com\/blog\/pg-stat-statements-four-queries-before-adding-an-index\/\" rel=\"noopener\">the four pg_stat_statements queries I run before adding any index<\/a> identify the guilty SQL. Time dimension from one, query dimension from the other. Between them you&rsquo;ve covered most of what a paid APM gives you for a database this size.<\/p>\n<p>What this stack won&rsquo;t do is page you at 3am. There&rsquo;s no alerting here, and I don&rsquo;t pretend otherwise. For the client work I do through <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my agency practice<\/a>, I pair it with a dumb uptime check and a disk space alert, on the theory that a small system&rsquo;s genuine emergencies are few and boring, while its performance mysteries are frequent and historical. The snapshots are for the mysteries.<\/p>\n<h2 id=\"set-it-up-before-you-need-it\">Set it up before you need it<\/h2>\n<p>The uncomfortable truth about snapshot-based monitoring: it only helps if it was already running before the problem started. You cannot retroactively collect history, and the day you wish you had it is by definition too late. So here&rsquo;s the concrete move for this week: pick your most important Postgres box, install the extension, set the timer for every 15 minutes, and forget about it. The whole exercise takes ten minutes. Next month, when someone says &ldquo;the app felt slow yesterday afternoon,&rdquo; you&rsquo;ll run one command and have charts instead of vibes.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>pg_statviz 1.2 adds Postgres 19 support and a blocking locks module. How I monitor client databases with snapshots and a systemd timer instead of a paid APM.<\/p>\n","protected":false},"author":2,"featured_media":630,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"pg_statviz 1.2 adds Postgres 19 support and a blocking locks module. How I monitor client databases with snapshots and a systemd timer instead of a paid APM.","rank_math_focus_keyword":"postgresql monitoring tools","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[237,302],"tags":[80,227,689,238,8],"class_list":["post-631","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-databases","category-devops","tag-devops","tag-monitoring","tag-pg-statviz","tag-postgresql","tag-self-hosting"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/631","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=631"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/631\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/630"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=631"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=631"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=631"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}