There's a persistent belief in the database world that vertical scaling solves all problems. Need more throughput? Add CPUs. Running out of cache? More RAM. Queries hitting disk? Higher IOPS. It's a comforting philosophy because it's simple, and for a surprisingly long time, it works. A single beefy Postgres instance can handle an enormous amount of punishment before collapsing under the strain.But there's a ceiling up there, and it's not made of hardware. Postgres was designed as a single-instance database engine, and many of its internal structures are shared across every database the instance contains. These shared resources are rarely concerning in a single modest instance. But with twenty databases running a mixture of heavy OLTP workloads, analytical queries, or even mostly idle, the shared nature of these internals becomes very relevant.Let’s talk about the barriers these over-provisioned instances eventually hit, with references to the Postgres source code itself for good measure. Some of these are well known, while others are the kind of thing that strikes suddenly at 2 AM when all the monitoring dashboards turn red simultaneously.
Postgres Conference 2026 was held in San Jose, California, and once again, I was lucky to be invited to speak. This is a great show for the 'hallway track' where you talk to members of the community and discover many interesting things.
I had a brief conversation with two early contributors to the original PostgreSQL project. One said he was surprised by how much of his code was still in the code base after FORTY YEARS.
AI and MCP are fully interlaced in many projects. The one that struck me the most was PgEdge's AI DBA Workbench. It features three tiers of detection and an agentic AI assistant to watch it all. I was talking with CEO David Mitchell about it and told him about my past frustrations with Percona's PMM. He then told me author of the AI DBA Workbench also wrote PMM. And PgAdmin4.
Observability seems to be the hot, underlying theme this year. Take a look at pg_collector. It is an SQL script that gathers valuable database information and consolidates it into a single HTML file, providing a convenient way to view and navigate the report's sections. One issue with PostgreSQL management is that it is a very complex product with about 400 different settings, and nobody can know in depth how they work. Pg_collector covers a lot of the territory by showing duplicate indexes, invalid indexes, unused indexes, autovacuum parameter, sequences with less than 10% of the remaining values, orphaned prepared transactions, connections without SSL, excessive logging parameters, track_counts parameter, synchronous_commit parameter, invalid databases, tables with more than 20% dead rows, transaction ID TXID (Wraparound), tables that have autovacuum_enabled=off on table level, inactive replication slots, logical replication spill files, and outdated extensions.
For more on observability, check out Ryan Booz's session from the second day, the one not about beekeeping.
This is being written on the morning of the last of three days. I needed to capture some of the highlights of the show before th
[...]PostgreSQL is dominating the database market, and the monitoring tools haven't noticed.More teams run Postgres in production every year. More of those deployments are distributed, multi-region, and mission-critical. And the tooling most of those teams rely on was built for a simpler world: a single instance, a handful of threshold alerts, and a senior DBA who can interpret what the graphs mean at 3 AM. That works when you have one cluster and one person who knows where the bodies are buried. It falls apart the moment you scale past either of those constraints.We built the pgEdge AI DBA Workbench to close that gap, and today it's entering public beta. We think it's the best PostgreSQL monitoring and management platform you've seen, and the rest of this post explains why any postgres 14+. Local installs, self-hosted enterprise estates, Supabase, Amazon RDS - If you can connect to it, you can monitor it.
A couple of months back, the CEO challenged product and marketing to revamp the developer experience on our website in three weeks. I vibe-coded a proof of concept full of "try it now" buttons and interactive guides, the CEO loved it, and then I had to deal with almost every one of those interactive guides being a placeholder card. Engineering was fully booked, and the Control Plane product I needed to write guides for was one I knew inside out at the architecture level but had never personally operated end-to-end through the API.So I sat down and learned the pgEdge Control Plane the hard way: by using it. What follows is what I found, organized as the guide I wish I'd had when I started. If you're evaluating Control Plane, deploying it for the first time, or trying to understand what Day 2 operations actually look like, this is for you.
On a PostgreSQL build with assertions enabled, run the standard make check-world suite with a small debugging extension called pg_pathcheck loaded. It will report on pointers to freed memory in the planner's path lists. Such dangling pointers exist even in the core Postgres now. They are harmless today. But the word today is what makes this worth writing about.
This story started in July 2021. At the time, I was finishing a sharding solution built on top of postgres_fdw. During testing, our engineers sent me an example query that would crash periodically with a SEGFAULT. One look at the plan told me something was very off.
Append
-> Nested Loop
Output: data_1.b
Join Filter: (g1.a = g2.a)
...
-> Materialize
Output: g2.a, data_2.b
-> Hash Join
Output: g2.a, data_2.b
Hash Cond: (data_2.b = g2.a)
...
The first obvious question: how did a Materialize node end up as a direct child of an Append? The second, more interesting one: how can one Append combine two sources with different tuple widths? No wonder the query was crashing — and to make it worse, the failure was intermittent; sometimes the very same query produced a perfectly reasonable plan.
On paper, the bug shouldn't have been possible: the optimiser doesn't work that way. A few days of debugging pointed the finger at dangling pointers. While building one of the alternative Append paths, the optimiser adds a cheaper path to a child RelOptInfo’s pathlist and evicts the one that was there before. But the previously constructed Append still holds a pointer to that now-freed slot. A step or two later, the allocator hands the exact same chunk back out for a new Path higher up the tree, for, say, an enclosing JOIN. The result is a plan that makes no semantic sense at all.
PostgreSQL builds each relation's pathlist incrementally through add_path(). When a newly arrived path dominates an e
REPACK is a new PostgreSQL 19 feature for physically compacting a table by rewriting it into new storage. Like VACUUM, it deals with the space left behind by dead tuples, but it does so by building a fresh table file instead of mostly cleaning pages in place. Ordinary VACUUM can mark space reusable
inside the table and may truncate some empty pages at the end, but it usually cannot fully return bloat to the operating system. REPACK, like VACUUM FULL, rewrites the table into a compact file and swaps that storage into place. The important difference from VACUUM FULL is that REPACK CONCURRENTLY keeps the table usable for most of the operation by copying a snapshot and replaying concurrent changes before a short final lock-and-swap phase.
REPACK code is interesting because it sits between several difficult subsystems: table rewrites, index rebuilds, relfilenode swaps, logical decoding, background workers, snapshots, and lock management. Reading repack.c is a good way to understand how PostgreSQL can physically rebuild a table while preserving the table’s logical identity.
At a high level, REPACK creates a new physical copy of a table, fills it with live tuples from the old table, rebuilds or swaps indexes, and then swaps the physical storage underneath the original relation OID. The user still sees the same table OID, privileges, dependencies, inheritance relationships, and catalog identity, but the heap file is new and compact.
The file comment at the top of repack.c summarizes the two modes:
AccessExclusiveLock, rewrite the table, swap storage, drop the old storage
ShareUpdateExclusiveLock, copy the table while writes continue, decode concurrent changes from WAL, replay them into the new heap, briefly upgrade to AccessExclusiveLock, apply remaining changes, then swap.
That split drives almost every design choice in the file.
The main SQL entry point is ExecRepack() in repack.c. It parses options like VERBOSE, ANALYZ
The London PostgreSQL Meetup Group met on April 14, 2026 organized by:
Monica Sarbu spoke at the The San Francisco Bay Area PostgreSQL Meetup Group met virtually on April 14, 2026 organized by
On April 15, the Postgres Meetup for All met virtually, organized by Elizabeth Christensen on April 15, 2026.
Speaker:
The Barcelona (+Valencia) PostgreSQL User Group met on April 17, 2026
Organizer:
Speaker:
On April 15, 2026, Ellyne Phneah delivered a PostgreSQL talk at the Digital Marketing Europe 2026.
Community Blog Posts:
Book Publications:
Everyone who’s gone at least knee-deep in photography knows there’s this idea of the exposure triangle: aperture, shutter speed, and ISO. Depending on what you’re going for artistically, you adjust the three parameters, knowing that there are trade-offs in doing so. After working on a few cases, and presenting solutions to customers, I’ve started to think about Postgres performance tuning in a similar way – there are basic parameters that can be tuned, and there are trade-offs for the choices DBAs make:
Each of these (in broad strokes) affects throughput – how much work your system gets done.
Caveat: I know that in the academic sense, “throughput” doesn’t quite capture the balance of these concepts, but please bear with me!
Let’s talk about how each of these three work together with the whole system, and what the trade-offs look like.
When you increase memory allocation in Postgres, whether it’s shared_buffers or work_mem, things tend to feel smoother. Most notably, queries spill to disk less often, sorts and joins stay in memory, cache hit rates improve. But there’s a trade-off that’s easy to miss at first, especially with these two parameters. A single complex query can consume multiple chunks of work_mem (see Laetitia’s excellent post about it). Multiply that across concurrent queries, and you begin to see the OS consuming swap space, churning at checkpoints, and even OOM Killer getting invoked. So while more memory can make things faster, it also quietly reduces how much concurrency your system can safely handle.
I’d relate this to aperture – you can throw money at some fast glass, but you also get shallower depth of field (in an annoying way).
Disk is where things go when memory isn’t enough, or when an access pattern requires it. We see examples of this in , sequential scans, random index lookups, and temporary files from sorts or hashes. Lowering work_mem might increase disk I/O due to so
You run SELECT * FROM orders in one psql session and see 50 million rows. A colleague in another session runs the same query at the same moment and sees 49,999,999. Neither of you is wrong, and neither is seeing stale data. You are both reading the same 8KB heap pages, the same bytes on disk.
This is the promise of PostgreSQL's MVCC (Multi-Version Concurrency Control), and it's the reason readers never block writers and writers never block readers. It is also one of the most misunderstood pieces of the storage engine. People know "there are multiple versions of a row" and leave it at that.
The answer lives in eight bytes on every single tuple.
If you've read Inside the 8KB Page, you know that every tuple starts with 23-byte header. The first eight bytes of that header are two 32-bit transaction IDs: t_xmin (the transaction that inserted this version) and t_xmax (the transaction that deleted or updated it, or 0 if it's still live).
That's the core of MVCC at the storage level. PostgreSQL does not keep a separate "current version" table. It does not mark rows as latest. Every tuple carries its own two-field timestamp, and when your query reads a page, PostgreSQL has to decide, tuple by tuple, whether your transaction is allowed to see it.
A minimal demo:
CREATE TABLE mvcc_demo (id int, val text);
INSERT INTO mvcc_demo VALUES (1, 'alpha'), (2, 'beta');
Peek at the raw page with pageinspect:
SELECT lp, t_xmin, t_xmax, t_ctid
FROM heap_page_items(get_raw_page('mvcc_demo', 0));
lp | t_xmin | t_xmax | t_ctid
----+--------+--------+--------
1 | 100 | 0 | (0,1)
2 | 100 | 0 | (0,2)
(2 rows)
Two tuples. Both stamped with t_xmin = 100 (the transaction that ran the INSERT) and t_xmax = 0 (nobody has deleted them). At this moment, every session on the database will see these rows, because everyone's snapshot agrees that transaction 100 has committed.
Now open two concurrent sessions. Session A runs an UPDATE without committ
[...]Postgres table partitioning is one of those features that feels like a superpower right up until it isn't. Just define a partition key, carve up data into manageable chunks, and everything hums along beautifully. And what's not to love? Partition pruning in query plans, smaller tables, faster maintenance, easy archiving of old data; it's a smorgasbord of convenience.Then you try to enforce a unique constraint without including the partition key, and Postgres behaves as if you just asked it to divide by zero. Well... about that.
I have just completed the first draft of the Postgres 19 release notes. It includes little developer community feedback and still needs more XML markup and links. This year I have created a wiki page explaining the process I use.
The release note feature count is 212, which includes a strong list of administrative and monitoring features. Postgres 19 Beta 1 should be released in a few months. The final release is planned for September/October of this year.
Number of posts in the past two months
Number of posts in the past two months
Get in touch with the Planet PostgreSQL administrators at planet at postgresql.org.