Postgres API and ABI stability has been a topic of discussion for a long time. To understand why this matters so much, start with Dave Stokes recent blog post [Is PostgreSQL Really Winning Part 2: The Evidence Beyond the Rankings]. It’s fundamentally about “a slow core with a fast edge is a pretty good design” and how the Postgres extension API enables an amazing ecosystem around Postgres.
The official postgres docs have [guidance on API/ABI stability], and for a bit of extra background check out [David Wheeler’s 2024 post-pgconf blog], and EnterpriseDB’s [explainer about the Postgres 17.1 ABI breakage].
The postgres community works really hard not to break ABI compatability because it means that a bunch of extensions won’t interoperate between minors. But sadly this was another of the rare cases where it was needed… this time as a result of CVE-2026-6460 (just one of many CVEs in the GIANT stack that 18.7 contains).
It’s explicit in the release notes and it’s the very first bullet point. https://www.postgresql.org/docs/release/18.7
Introduce versioning logic to permit detection of composite datums that don’t match the tuple descriptor we wish to interpret them with. For full protection, this change requires rebuilding C-language extensions, and there are some extensions that will require source-code changes. If necessary, the new composite_type_mismatch server parameter can be used to disable these checks, but doing so carries security risks.
This will be a pain for Postgres users. It means that extensions (which are packaged and distributed separately) need to be upgraded exactly in lockstep with the Postgres minor bugfix update itself.
One very unfortunate side-effect of the Postgres model: Postgres 18.7 has important CVE fixes – but people using extensions cannot upgrade to 18.7 and get those CVE fixes until their extensions are also updated and have released new extension versions.
I’m doing a lot of work around extensions for CloudNativePG on Kubernetes and I saw a lot of my
[...]Last month, the Apache Software Foundation's Community Over Code came to Scotland 🏴, taking place from October 11th to 14th at the Hilton Glasgow. Even though I've been in the open source world for 25+ years, this was my first Community Over Code (!), and I loved it: great talks, an amazing hallway track with Apache community rockstars, and a Community track that I very much enjoyed. I also came away convinced that there is huge potential for synergy between the Apache data ecosystem and the PostgreSQL community.
Many thanks to the ASF and everyone who made Community Over Code Glasgow 2026 possible.
ColdFront didn't exist yet when the Call for Presentations closed, so I submitted it as a lightning talk at the last minute. On Wednesday, October 14th, I had the pleasure of presenting ColdFront: Transparent PostgreSQL Tiering to Iceberg. It was also my first lightning talk under these rules: five minutes, no laptop, no slides! Many thanks to Ruth Suehle and Brian Proffitt of the ASF for hosting.
ColdFront is one example of that synergy: it moves data from PostgreSQL to Apache Iceberg transparently, so your application keeps using plain PostgreSQL. We covered:
UPDATE, MERGE, ALTER TABLE and everything else you would expect, that successfully runs all 99 queries of TPC-DS.
The rec
[...]
PostgreSQL 18.7, 17.12, 16.16, 15.20 and 14.25 are out today. If you use the PGDG RPM repositories (yum.postgresql.org or zypp.postgresql.org), please update PostgreSQL and all of your extensions in the same transaction this time. Do not update the server today and the extensions next month, and do not hold one back while the other moves.
This release is different from a usual minor release. It contains the fix for CVE-2026-6460, and that fix reaches into the extensions too. So we rebuilt every extension in the repositories against the new minor releases. That covers every package built separately for each PostgreSQL version, on all supported RHEL / Rocky Linux / AlmaLinux, Fedora, Amazon Linux, SLES and openSUSE Leap versions. The rebuilt packages are released together with the new minors.
The CVE-2026-6460 fix (commit 245eb832f) adds versions for composite types. It changes how composite values are stored, and it changes the server's interface to extensions in two ways:
DatumGetHeapTupleHeader() used to be a macro. It is now a function in the server.
heap_getattr() now calls the new heap_datum_check().
The headers are compiled into the extension, so this affects both directions:
So a new server needs new extensions to be safe, and the new extensions need a new server to load at all. That is why they have to be updated together.
Until now, an extension package only required the PostgreSQL major version. From these rebuilds on, every co
[...]
Build Vercel Cron Jobs in Next.js with CRON_SECRET, PostgreSQL locks, bounded backfill, safe retries, and a practical production debugging guide.
Your daily report runs at 02:00 UTC. One morning it never reaches the app. The following day it arrives twice, and both invocations try to produce the same report. A route that processes “today” on each request skips the missing day and may repeat yesterday’s work. The fix is to record which days are due and which were completed in PostgreSQL.
This guide builds a Next.js cron endpoint that queues each closed reporting day once, catches up after missed invocations, and can resume after a crash. The example writes daily event counts to PostgreSQL. A later section explains what changes when a job sends email or calls an external API.
Vercel Cron Jobs invoke a production route with an HTTP GET on a UTC schedule. Put the configuration in the root directory of the Vercel project. In a monorepo, that is the configured app root, which may differ from the repository root. This example schedules 02:00 UTC daily; the plan limits list minimum intervals and timing precision. A Hobby job may run within the scheduled hour, so do not depend on an exact minute.
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"crons": [
{
"path": "/api/cron/daily-report",
"schedule": "0 2 * * *"
}
]
}
Set a random CRON_SECRET in Production. Vercel sends it as a Bearer token. Fail closed if the variable is missing, and never log the header. The cron management docs explain how deployed environment values, rotations, and rollbacks interact. After changing the value, deploy and verify the next invocation.
A request that reaches the route is a wake-up signal. It can be late, duplicated, or absent; Vercel does not retry a failed cron invocation. The delivery guidance recommends reconciliation. Keep the schedule as a trigger and put the actual reporting state in the database.
Build PostgreSQL vector search with pgvector and Next.js. Compare keyword and hybrid ranking, enforce tenant access, and reproduce filtered HNSW recall.
A support engineer searches for AUTH-4017. The most useful result is the ticket with that exact error code. Another engineer types “sign-in works until I close the browser” and wants the ticket about a session cookie disappearing on restart. These queries need different kinds of evidence. A useful search feature can combine both without sending the entire support archive to a model.
This guide builds keyword, vector, and hybrid retrieval in PostgreSQL, then connects the query to a Next.js Route Handler. The lab uses fixed, three-dimensional vectors so you can reproduce the ranking and filtering behavior without an embedding API key. Those numbers stand in for embeddings; they do not measure an embedding model’s understanding of support tickets.
Use a disposable PostgreSQL database with pgvector 0.8 or newer. A provider that supports the extension or a local image from the pgvector project works. The extension and schema setup require a migration role; the search route will use a separate, restricted login.
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE support_ticket (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id text NOT NULL,
error_code text,
title text NOT NULL,
body text NOT NULL,
embedding vector(3) NOT NULL,
search_text tsvector GENERATED ALWAYS AS (
setweight(to_tsvector('english', title), 'A') ||
setweight(to_tsvector('english', body), 'B')
) STORED
);
CREATE INDEX ticket_tenant_idx ON support_ticket (tenant_id);
CREATE INDEX ticket_code_idx ON support_ticket (tenant_id, error_code);
CREATE INDEX ticket_fts_idx ON support_ticket USING gin (search_text);
INSERT INTO support_ticket (tenant_id, error_code, title, body, embedding)
VALUES
('acme', 'AUTH-4017', 'AUTH-4017: OAuth callback failed',
'Verify the redirect URI re[...]
A PostgreSQL database holds logic as well as data, in functions, triggers and Row-Level Security (RLS) policies. On Supabase and Neon much of that logic can be reached straight from the browser. UnitAutogen for PostgreSQL generates three kinds of test for it, together with the mock data each test needs, so nothing is written by hand.
So far, the storage internals series has focused on heap pages. We began by opening the 8KB page, then traced MVCC visibility through xmin, xmax, and snapshots. Next we observed HOT updates evade index maintenance, and finally tracked VACUUM as it went through its phases. Each time an index came up, the explanation stopped at either "the index points to the heap via ctid" or "index cleanup removes the dead entries". This chapter is about what lies at the other end of that pointer.
B-tree pages use the same 8KB skeleton (24-byte header, line pointers, tuples packed from the bottom up), but where a heap page is an unordered bag of row versions, a B-tree page is sorted and linked to its siblings at the same level. VACUUM cannot undo index bloat for that reason; the operational side of this issue is covered in VACUUM Is a Lie. Index bloat is a recurring topic and source of question during my talk "Postgres Storage Internals Visualized", and this is my attempt to address it better.
We need pageinspect for reading raw B-tree pages and pgstattuple for index-level statistics. By default, both require a superuser, but on managed instance, only pgstattuple can be granted because pageinspect checks for a superuser inside every function. All of the following was captured on PostgreSQL 18.6 (the postgres:18 image) in a single session; page numbers and item counts are from that run.
CREATE EXTENSION IF NOT EXISTS pageinspect;
CREATE EXTENSION IF NOT EXISTS pgstattuple;
CREATE TABLE idx_demo (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
value integer NOT NULL
);
INSERT INTO idx_d[...]
On 4th of November, 2026, we had the PostgreSQL November Meetup in Berlin. AWS hosted it again, this time we had two speakers from Germany and UK.
The Meetup took place in the Amazontower (EDGE East Side Tower Berlin) in Berlin, across the Uber Arena and right next to the East Side Mall.
Oleksii opened the Meetup with a few general notes
A backend keeps its mapping of the arena in its cache. As a result, if another backend drops or truncates the table, the memory of the old arena is not returned until that backend disconnects, so the cache has to be invalidated when the relation changes. This post uses the relcache invalidation messages of Postgres to find out when a table has changed, shows why the callback can only remember what has to be done, and detaches the mappings when the transaction ends.
“There are only two hard things in Computer Science: cache invalidation and naming things.”
Phil Karlton
In Part 7 any backend could find the arena of a table, and the mapping was kept in the cache entry of the relation so that it only had to be made once. A comparison of the handle of the entry with relfilenode made sure that a backend that had read a table started to use the new arena when another backend truncated the table. That comparison runs when the table is used, though, so it says nothing about the backends that do not use the table again. This post fixes that, and the companion code is the code from Part 7 with the additions described below.
Note: There are a few things in this cache invalidation implementation that I am not fond of and will dig into later, but the current implementation is good enough to explain how invalidation works and how it can be implemented.
Before delving into the nuances of the cache and invalidations, a function for inspecting the cache would be useful. You can add a simple function and view that allows you to view the cache entries of the calling backend. The function is a set-returning function (usually called SRF) that is then used by the view. The view returns the information in the cache, but also the relation name and relfilenode (where the handle is stored) from pg_class.
CREATE FUNCTION memtable_cache_entries(OUT relid oid, OUT handle oid,
OUT mapped bool)
RETURNS SETOF record
AS [...]
AI-First, AI-Ready, AI-Accountable, AI-Value-Driven. Four phrases now crowd enterprise strategy decks. They aren’t stages, and they aren’t interchangeable. They’re four questions every leader needs to answer about the same piece of work.
In more than twenty years of building data platforms for banks, insurers, telecoms, and healthcare organizations, I’ve rarely met an enterprise with an AI vocabulary problem. Most have an AI meaning problem.
Watch a leadership team over a single quarter, and you’ll see it. The organization declares itself AI-First in the town hall, asks whether it’s AI-Ready in the architecture review, worries about being AI-Accountable in the risk committee, and promises the board it will be AI-Value-Driven. Four phrases, four meetings, often four different owners, and rarely a shared understanding of how they relate. Are they stages to climb? Different ways of describing the same ambition? Slogans that change depending on who’s presenting?
They’re none of those. Each is a distinct executive question about the same piece of work. An organization that answers one and ignores the others ends up with an impressive demonstration, a production system nobody fully trusts, or a business case that quietly falls apart once the real costs surface.
I explored these distinctions in my LinkedIn series, Beyond AI Buzzwords, which built on my earlier series, What AI-Ready Actually Means. This article brings that thinking together and takes the argument further. An enterprise isn’t transformed by AI because it deploys more intelligence. It’s transformed when it can rethink how work gets done, execute reliably, govern the authority it delegates, and demonstrate sustained business value.
Abstractions are easy to agree with and harder to act on, so let’s examine all four terms through a single, ordinary business process: a customer refund.
It’s unglamorous, which is precisely why it’s useful. A refund touches customer reco
[...]
No, this is not in commemoration of the late Tim Curry. After repeatedly writing about the Swiss PGDay, I want to share a personal impression from the European PostgreSQL conference 2026.
PGConf EU is the main European PostgreSQL conference, and one of the biggest (or the biggest?) worldwide. With upwards of 700 attendees, it is a crowd rather than a family reunion. It attracts people not only from all parts of Europe, but from all around the world. The conference is held in a different city every year, and it is the place to go if you want to feel how strong PostgreSQL has grown over the years (the 2010 PGDay — the first I attended — was a cosy little event in comparison). In 2026, there are five parallel tracks of talks, and still I have to count myself lucky that my talk was accepted!
Slonik (from Slavic “slon”, which means “elephant”) is the name of the elephant logo and mascot of PostgreSQL. Now the PostgreSQL license is pretty liberal, but the project protects its trademarks with teeth and claws. Among them is the Slonik logo (the name “PostgreSQL” is also protected, but its use in blogs is explicitly allowed).
Slonik isn't only a logo. The elephant regularly makes an appearance at PostgreSQL conferences, greeting the crowd and posing for pictures. There are two mysteries around Slonik:
This blog is about the second question.
PostgreSQL conferences wouldn't be possible without the work of the organizers. Amazing as the work of this group is, you cannot run a conference of that size with just eleven people. It takes a lot of volunteers to take care of everything: the registration desk, room hosts, cleaning up, etc. Volunteering at a PostgreSQL conference is a great way to get to know the peop
[...]If you had told me in April 2026 one question “Mehmet, can we use managed PostgreSQL for this project?” would send me into the deepest rabbit hole of my life, I would have laughed.
Months later, I finally found the bottom of that rabbit hole. It was a 22-year-old zero-day in PostgreSQL core.
In this blog post you will read a story of how I ended up uncovering a new class of type-confusion vulnerabilities in PostgreSQL.
This is the 3rd part of blog post series named “Systemic Risks in the Managed PostgreSQL Industry”. For those who have missed the first two part, feel free to start from 1st and 2nd which will give you a complete context of this madness.
TLDR:
I gave PostgreSQL a value inside a composite type, changed the type of one of its fields, and asked it to read the value back. That type confusion led to one of the most reliable memory-corruption exploit I have seen. But I did not start by looking for a PostgreSQL core vulnerability.
I was exploiting pg_clickhouse, and getting that exploit to work left me with a question about how PostgreSQL understands its own data. With an AI agent as my teacher, I worked through the internals one question at a time. Then a seemingly unrelated conversation gave me the idea that connected it all, and eventually I found myself staring at a primitive that had been there for for awhile.
This is the full true story of how those pieces came together, I am sure you will enjoy this story.
During June and July, I had been exploiting memory-corruption vulnerabilities across several PostgreSQL extensions. One of the extensions that I achieved code execution was pg_clickhouse. That case study has its own story, which I will share in the future.
What matters here is what that exploit forced me to learn. pg_clickhouse translates ClickHouse values into native PostgreSQL values. A ClickHouse Array is not a PostgreSQL array, right ? So the extension had to rebuild its elements as Datum values in the for
On 4 November, the PostgreSQL Meetup Group Berlin met.
Organized by:
Speakers:
On 6 November, Claire Giordano and Aaron Wislang hosted and published a new podcast episode “How I got started in Postgres with Magnus Hagander from the Talking Postgres series.
On 6 November, the Hyderabad PostgreSQL User Group met, organized by
Speakers:
Multimedia contributions:
Also posted in Microsoft Blog for PostgreSQL
I recently worked with a customer planning a migration from on-premises PostgreSQL to Azure Database for PostgreSQL. During performance testing, they noticed something troubling: a query that completed in tens of milliseconds would now take 1-2 seconds in the Azure environment being evaluated.
The query was using an index scan, and running it again was fast. But once the relevant data was evicted from cache, the long execution time came back. Naturally, the conversation turned toward storage performance. There was a clear difference in IOPS consumption, with the to-be-production server having 10-20% more IOPS consumption than a server they stood up for comparison purposes. A cold-cache query execution that performed a single Index Scan pulled over 100 pages from heap, whereas the other server only pulled 60 pages. The natural inclination was to think that somehow bloating and/or fragmentation was the cause, so they tried VACUUM FULL but did not see any improvement. That’s when they turned to us.
The primary concern that the customer had – an Index Scan was not performing as well as expected – surfaced two hypotheses: something was wrong with the index, (i.e., bloat, leaf density, fragmentation), or something was wrong with the storage. Digging in with pg_stattuple and pg_statindex, we didn’t see anything glaringly wrong with leaf density or fragmentation. We also saw nothing from our telemetry that anything was wrong with the storage. With the usual suspects out of the picture, we needed to get back to the basic principles of indexing.
It is important to remember that when it comes to indexing a table, a regular index scan uses the index to locate matching tuples, then visits the table’s heap pages to retrieve them. The index is ordered; the heap doesn’t have to be.
Suppose a query returns 200 rows. If those rows live on five heap pages, Postgres can retrieve t
[...]In the previous post I argued that migrations don’t earn Git’s protections by being made of files: two incompatible schema changes live in two separate migration files, so there is nothing to textually conflict, and both branches merge clean. The repository accepts the pair without comment.
That post ended with a claim: the collision is not resolved in the repo, so it must be resolved somewhere else — at apply time, per environment, inside the database. This post is that claim, run for real. What actually happens when the pair lands?
I ran the collisions on PostgreSQL 16 to find out. The answer comes in two flavors — one loud, one silent — and the silent one is worse.
None of what follows is an argument against migrations, or against the defensive idioms people write in them. Every tool below does exactly what it promises. The point is what those promises do and don’t cover once two branches meet.
Branch A adds a column to users. Branch B renames users to accounts. Semantically incompatible; textually invisible to Git; both merged.
Each environment applies the migrations in whatever order they arrive — and the order can differ between environments, because branch merge order and migration timestamp order are two different clocks. So I built two databases and applied the pair in both orders:
-- env_one: add column, then rename
ALTER TABLE ✓
ALTER TABLE ✓
-- env_two: rename, then add column
ALTER TABLE ✓
ERROR: relation "users" does not exist
env_one is fine. env_two is broken — and its migration run has now halted partway, with everything after the failed file left unapplied.
Note what this means operationally: the same pair of migrations produces a healthy database in one environment and a failed deploy in another, decided by application order alone. If your staging environment happened to apply them in the survivable order, staging is green and the failure is waiting in production. The
[...]These are thirteen things I measured in one sitting. Each one is small. A few of them surprised me anyway.
PostgreSQL 18.6, built from source, default settings (work_mem 4MB, shared_buffers 128MB, 2 parallel workers per gather). MacBook Air M5, 10 cores, 24 GB RAM, internal SSD. This is a different machine from my earlier posts, so don't compare times across posts.
Most tests use one table of 2,000,000 orders: 50,000 customers, 5% of rows with status open, created_at increasing every 10 seconds, and an md5 string in note. Query times are EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) execution time, median of 5 runs after one warm-up run (3 runs for the slower ones). Buffers are from the top plan node. Sizes are decimal MB. The script and the raw results are linked at the bottom.
A partial index only holds the rows its WHERE clause allows.
| Index on customer_id | Size | Buffers for one customer's open orders |
|---|---|---|
| all rows | 14.2 MB | 43 |
WHERE status = 'open'
|
0.7 MB | 2 |
It only gets used when the query's WHERE matches the index condition.
BRIN stores a summary per block range instead of one entry per row. Rows were inserted in created_at order, which is the case it is built for.
| Index on created_at | Size | Count one day (8,640 rows) |
|---|---|---|
| B-tree | 44.9 MB | 0.55 ms, 134 buffers |
| BRIN | 24 kB | 0.63 ms, 137 buffers |
The table itself was 202.3 MB. I didn't test BRIN on shuffled
[...]My last post leaned hard on the DB-Engines ranking, and I admitted that ranking measures popularity, not installed base or revenue. Several of you called me on that, politely and otherwise. Fair enough.
So this time I went looking for evidence that does not come from one ranking site. Usage surveys: where the money is going, what cloud vendors are building, and what AI tools reach for when nobody tells them what to use. My conclusion has not changed, but I think the case is much stronger than I made it out to be.
The best usage number I can find comes from developers themselves. In the 2025 Stack Overflow Developer Survey, 55.6% of all respondents said they use PostgreSQL, up from 48.7% in 2024. Among professional developers it was 58.2%, roughly 18 points ahead of MySQL (summary of the survey). It also topped the admired and desired lists again.
That is still a survey, not an install count. But it is people saying "I use this," not people searching for it, and that is a different signal from the one DB-Engines measures.
Three caveats on my own napkin math from last time:
Last time I wrote that big data and AI vendors have been buying PostgreSQL companies, and I did not name them. Here they are.
|
Date |
Bu |
|---|
This year's PostgreSQL Conference Europe, PGConf.EU 2026, took place from October 20th to 23rd in wonderful Valencia, at the Palacio de Congresos. There was far too much going on to cover it all, so I'm just going to go over what I was involved with: a panel on Tuesday, a book signing on Wednesday, a talk on Thursday, and three Community Events on Friday. That still leaves out the hallway track, and the many discussions there on everything from Postgres heisenbugs to the core development workflow to community building.
Obligatory selfie from PGConf.EU 2026
On Tuesday afternoon I joined Ants Aasma, Mayuresh Bagayatkar, Hettie Dombrovskaya, Pavlo Golub and Teresa Lopes for Postgres On-Call Confessions: Worst Practices 101, a panel on the "Postgres in production" topics everyone shies away from. We started with the common threads in where we've seen Postgres fail at scale and in production, but also the most embarrassing and funniest incidents each of us has personally witnessed. Then the mic was open for anyone in the audience to come up and confess an "obvious fix" of their own that backfired in production.
The Postgres On-Call Confessions panel, left to right: Pavlo Golub, me, Mayuresh Bagayatkar, Ants Aasma, Teresa Lopes and Hettie Dombrovskaya
On Thursday morning I had the pleasure of presenting radar + radar-analyst: Agentless Diagnostic Snapshots for PostgreSQL.
The starting point is familiar to anyone who has worked in support. A monitoring agent only helps if it was already running before things went wrong, the Postgres log only shows what your queries did, and the rest of the picture takes a dozen emails back and forth. radar is a single static Go binary that collects a snapshot of PostgreSQL and the host it runs on into one zipfile, in seconds, and then exits. It collects metadata only (catalogs, statistics and configuration), and never row data or passwords.
radar-analyst is what you do
[...]In my last post I walked through a rolling major version upgrade: add a brand new node on the new version, let it join the Spock cluster, retire the old node. It works well, but it assumes you can stand up an extra server for every node you need to upgrade. A lot of customers I talk to cannot. Either the hardware is not there, or the database is too big to hold a second full copy.For them there is a second option. Upgrade each node in place with . The node being upgraded does go down, but in a multi-master cluster the other nodes keep taking writes the whole time, so the application never loses a writable endpoint. The downtime belongs to one node, not to your users.I tested this end to end on the same lab I used for the rolling post, taking one node from PostgreSQL 17.10 to 18.6, including its Patroni managed standby. It worked, but not without a few surprises that the standard playbook will not warn you about. This post covers exactly what I ran.
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.