Skip to content
dbexplore

PostgreSQL 17

Every SLRU cache but one changed its name

For anyone whose cache panel is keyed on a string literal typed years ago.

Reference page, revised in place. Last updated .

The rename that is in the rows rather than the columns

Most of the breaking monitoring changes in a PostgreSQL release are column renames, and a column rename is loud: the server refuses the query and names the column it could not find. This one is different, and the difference is the entire reason it is worth a page.

pg_stat_slru reports on the small fixed-size caches the server keeps in front of several on-disk structures: the commit log, the subtransaction map, the multixact files, the notification queue. Its columns are unchanged in 17. What changed is the contents of the name column, which is what every query against this view filters on. The abbreviations the server had used internally for years were replaced with spelled-out names, so Xact became transaction, Subtrans became subtransaction, CommitTs became commit_timestamp, and the multixact and notification entries were lowercased into the same style. One row kept its name: other, the bucket for anything not separately tracked, and that detail matters more than it looks.

The rename sits under Migration to Version 17 in the PostgreSQL 17 release notes, which also records the part that surprises people: the names accepted by the reset function changed with them.

The same two calls on each server

SELECT name FROM pg_stat_slru ORDER BY name;
      name       
-----------------
 CommitTs
 MultiXactMember
 MultiXactOffset
 Notify
 other
 Serial
 Subtrans
 Xact
(8 rows)
SELECT name FROM pg_stat_slru ORDER BY name;
       name       
------------------
 commit_timestamp
 multixact_member
 multixact_offset
 notify
 other
 serializable
 subtransaction
 transaction
(8 rows)

Now a filter of the kind that sits inside every collector, asking for the commit-log cache by the name it has had since the view was introduced. On 16 it answers.

SELECT name, blks_hit, blks_read, blks_zeroed FROM pg_stat_slru WHERE name = 'Xact';
 name | blks_hit | blks_read | blks_zeroed 
------+----------+-----------+-------------
 Xact |     3822 |         3 |           2
(1 row)

On 17 it answers just as successfully, and with nothing in it.

SELECT name, blks_hit, blks_read, blks_zeroed FROM pg_stat_slru WHERE name = 'Xact';
 name | blks_hit | blks_read | blks_zeroed 
------+----------+-----------+-------------
(0 rows)

No error, no warning, no clue. A collector running that query publishes no series, an aggregation over it returns null, and a panel that sums the result shows a flat line at zero. Everything downstream behaves exactly as it would on a cluster whose commit-log cache is doing no work at all, which is a state a healthy cluster can genuinely be in, so nothing about the graph looks wrong.

The reset call that clears a cache you did not name

The second half is worse, because it writes rather than reads. pg_stat_reset_slru takes the name of a cache and clears that cache’s counters. Give it a name it does not recognise and it does not complain; the unrecognised name falls into the other bucket, and that is what gets cleared.

On 16, resetting everything and then asking for one cache by its old name does what it says.

DO $$
BEGIN
  PERFORM pg_stat_reset_slru(NULL);
  PERFORM pg_sleep(1);
  PERFORM pg_stat_reset_slru('Xact');
END $$;

SELECT name,
       stats_reset = (SELECT max(stats_reset) FROM pg_stat_slru) AS cleared_by_the_last_call
FROM pg_stat_slru
ORDER BY name;
DO
      name       | cleared_by_the_last_call 
-----------------+--------------------------
 CommitTs        | f
 MultiXactMember | f
 MultiXactOffset | f
 Notify          | f
 other           | f
 Serial          | f
 Subtrans        | f
 Xact            | t
(8 rows)

The same sequence on 17, with the new form of the wholesale reset:

DO $$
BEGIN
  PERFORM pg_stat_reset_shared('slru');
  PERFORM pg_sleep(1);
  PERFORM pg_stat_reset_slru('Xact');
END $$;

SELECT name,
       stats_reset = (SELECT max(stats_reset) FROM pg_stat_slru) AS cleared_by_the_last_call
FROM pg_stat_slru
ORDER BY name;
DO
       name       | cleared_by_the_last_call 
------------------+--------------------------
 commit_timestamp | f
 multixact_member | f
 multixact_offset | f
 notify           | f
 other            | t
 serializable     | f
 subtransaction   | f
 transaction      | f
(8 rows)

The call succeeded and it cleared the wrong row. A nightly job that resets the commit-log counters so the morning report covers one day is, on 17, resetting a catch-all bucket while the counters it meant to clear keep accumulating since the upgrade. The report is not empty and it is not obviously wrong; it is a total over the wrong window.

Why this one has no error to catch

It is worth being precise about the mechanism, because “it fails silently” is the kind of phrase people nod at without changing anything.

Both halves are silent for the same reason: the name is data, not an identifier. A column that does not exist cannot be resolved and the parser stops. A string literal that matches no row is an ordinary empty result, and a string literal handed to a function that treats unknown input as a catch-all is an ordinary successful call. Nothing in the server has a way to know you meant a cache that used to be called that.

There is one loud version of this change and it is worth knowing about because it is the exception that makes the rest look safe. The reset targets accepted by pg_stat_reset_shared are validated, so a bad one there does raise an error and names the accepted list. That is a different function with different behaviour, and the contrast catches people out: one function checks its argument and the neighbouring one does not.

What to change before the upgrade

  • Grep monitoring configuration for the seven old names as string literals. They turn up in exporter queries, dashboard variables, alert rules and any script that calls the reset function.
  • Prefer a query with no name filter where you can, aggregating or grouping by whatever the server reports, so the set of caches is discovered rather than asserted. This survives the next rename too.
  • Where a filter is genuinely needed, branch on the server version rather than matching both spellings, because matching both hides the moment the old one stops being right.

A collector that already scrapes the whole view and labels each series by name fares best here. Its series names change at the upgrade, which breaks continuity on a graph but does so visibly, and continuity is the cheaper thing to lose.

What the view costs to read

Nothing gates it and nothing needs a restart. The counters are maintained in shared memory whether or not anyone reads them, and the view is a handful of rows assembled on request.

Whether the numbers are interesting is a separate question. These caches are small and mostly invisible until a workload makes one of them hot, and the two worth watching are the subtransaction cache on a workload that nests savepoints deeply and the multixact caches on a workload that takes many concurrent row-share locks. Both show up as reads climbing against hits, and both are now reported under names that do not match the query you wrote.

Put every Postgres you run on autopilot.

We onboard teams in small batches. Tell us about your fleet and we will reach out when a seat opens. One email, no drip campaign.