Infrastructure engineering for East Africa's operators, platforms and regulators See the work →
PostgreSQL 14 End of Life: Upgrading to 18 on Ubuntu
Linux 7 min read

PostgreSQL 14 End of Life: Upgrading to 18 on Ubuntu

PostgreSQL 14 end of life is 12 November 2026. We upgraded an Ubuntu 22.04 server to 18: two refusals, a 433-second default and lost extended statistics.

CE
CloudSpinx Engineering
Infrastructure Engineering, Virtualization, Networking, Automation
23 October 2026
postgresqlpostgresql 14 end of lifepg_upgradeubuntu

apt install postgresql on Ubuntu 22.04 still installs PostgreSQL 14, and PostgreSQL 14 end of life is 12 November 2026, when the project ships its last minor release for it. After that, a security bug in 14 gets no fix from upstream. So we built that server, loaded 3.1 GB and the pgvector extension onto it, and upgraded it to 18 five different ways.

Upgrades like this are part of the managed database service CloudSpinx runs for organizations in Kenya, on their own servers and in their cloud accounts. The run refused twice before it started, Ubuntu's upgrade command took 433 seconds where one flag made it 12, the two fastest methods made a backup the only way back once 18 was running, and every pg_upgrade mode we ran dropped part of what the planner knows. The commands and output are below.

What PostgreSQL 14 end of life changes on 12 November

PostgreSQL supports each major version for five years, then ships a final minor release and calls it unsupported. Nothing stops working on the day. What stops is the patch. Ubuntu's jammy-updates carries 14.24, the current minor, and the 12 November release is the last one upstream makes, so the next CVE after it lands on a version the PostgreSQL project no longer fixes. If you also run Kubernetes, its clock is far shorter, about 14 months per minor.

Amazon RDS gives 14 a few more months. Its release calendar keeps 14 on standard support to 28 February 2027, and Extended Support pricing starts on 1 March 2027. A server you run yourself gets no further fixes from the PostgreSQL project after 12 November, only whatever your distribution decides to backport, and on our server, adding PGDG for 18 replaced Ubuntu's 14 build with PGDG's, which follows upstream.

Go to 18. It is supported to 14 November 2030, its current minor is 18.6, and it is the first version whose pg_upgrade carries optimizer statistics across, which matters more than it sounds (see the statistics section below). PostgreSQL 19 was at its first release candidate on 15 October, and a major that new is not what you put under an end-of-life deadline.

The two refusals in the upgrade from PostgreSQL 14 to 18

We added the PGDG repository, installed postgresql-18, created an 18 cluster with initdb's defaults and ran pg_upgrade --check against the stopped 14 cluster. It failed in a tenth of a second.

"old cluster does not use data checksums but the new one does"

Performing Consistency Checks
-----------------------------
Checking cluster versions                                     ok
old cluster does not use data checksums but the new one does
Failure, exiting

PostgreSQL 18's initdb turns data checksums on by default, and the 18 release notes say pg_upgrade requires both clusters to match. The cluster Ubuntu created for 14 has them off. You have two fixes. Create the new cluster with initdb --no-data-checksums, which costs nothing and leaves you without checksums. Or stop 14 and run pg_checksums --enable on it first, which rewrote every file of our 3.1 GB database (403,803 blocks) in 61.7 seconds, offline. That time grows with your data.

"Your installation references loadable libraries that are missing from the new installation"

Checking for presence of required libraries                   fatal
Your installation references loadable libraries that are missing from the
new installation.
...
could not load library "$libdir/vector": ERROR:  could not access file "$libdir/vector": No such file or directory
In database: shop

Every extension that loads a library needs that library built for 18 and installed before pg_upgrade runs, and pgvector 0.8.7 was installed for 14 only. After apt install postgresql-18-pgvector the check passed in 0.7 seconds. PostGIS and anything you compiled yourself go through the same library check, so list select extname, extversion from pg_extension in every database before the night, not during it.

pg_upgradecluster defaults to a dump

On Debian and Ubuntu the wrapper is pg_upgradecluster, from postgresql-common, which moves to version 293 when the PGDG packages for 18 go in. Its man page says it plainly: "The default is dump." We ran pg_upgradecluster -v 18 14 main on the kind of Ubuntu server we run and it took 432.9 seconds, including the analyze it runs at the end. For the dump itself it restarts the old cluster "with restricted connections", so plan the whole dump as the outage. pg_upgradecluster -m link -v 18 14 main did the same upgrade in 11.6 seconds, analyze included.

The wrapper earns its keep in two ways. In link mode it passed --no-data-checksums to initdb for us, so the first refusal never happened, and both runs finished by analyzing the new cluster. The dump has a merit too: it rebuilt everything and came out with checksums on. For a few gigabytes and a Sunday that trade is fine. A dump rewrites every row and rebuilds every index, so its time follows the size of what you have.

Raw pg_upgrade gives you the choice directly. We timed copy, link and swap, each several times against an identical copy of the 14 cluster, and marked where pg_upgrade's own output and documentation say each one stops leaving the old cluster usable. The link-mode refusal is the one we tested.

When each pg_upgrade mode stops leaving PostgreSQL 14 usable
When each pg_upgrade mode stops leaving PostgreSQL 14 usableThree pg_upgrade transfer modes from PostgreSQL 14 to 18, with the time each took on our 3.1 GB lab database and, per pg_upgrade's documentation, the stage at which the old 14 cluster can no longer simply be started. Copy took 23.5 to 52.4 seconds and the old cluster starts at every stage. Link took 3.1 to 15.5 seconds; once linking starts, the old cluster starts only after pg_control.old is renamed back, and once 18 has been started it needs a restore from backup. Swap took 9.4 to 14.1 seconds and the old cluster is unsafe from the moment the file transfer begins.checksschema restorefile transfer18 started--copy23.5 to 52.4 sstartsstartsstartsstarts--link3.1 to 15.5 sstartsstartsrename .oldbackup only--swap9.4 to 14.1 sstartsstartsbackup onlybackup only
  • old 14 still starts
  • starts after renaming pg_control.old
  • restore from backup
Of the three modes we ran, the faster the mode, the earlier the old cluster stops being a way back. Link finished in 3.1 to 15.5 seconds, and afterwards our 14 cluster would not start: pg_upgrade had renamed its pg_control.

Copy is the only one of the three that copies the data files, so its time follows the bytes on disk. Link and swap buy their speed by handing the same files to 18.

The link-mode warning is in pg_upgrade's own output, "the old cluster cannot be safely started once the new cluster has been started". We started 18, then tried 14. pg_ctlcluster 14 main start failed, and 14's log said why:

but could not open file "/var/lib/postgresql/14/main/global/pg_control": No such file or directory

That refusal is a guard, not the damage. pg_upgrade renames that file to pg_control.old when linking starts, before 18 ever runs. The pg_upgrade documentation says renaming it back is safe only if the new cluster never ran, and otherwise the old one "will need to be restored from backup". So link mode is the right choice when you hold a backup you have actually restored, which is a recovery test rather than a cron job, and the wrong one when nobody has opened the backup since it was set up.

If deciding which mode your data can afford is the part you would rather hand over, send us the pg_lsclusters output and the extension list and we will tell you.

PostgreSQL 18 keeps column statistics, not the ones you created

After a pg_upgrade to 18 in each of copy, link and swap mode, the column statistics from 14 were still there (pg_stats returned 18 rows, as it did on 14), which is the change 18 made to pg_upgrade. The one statistics object we had created with CREATE STATISTICS came back empty, and the documentation says so: statistics "created explicitly with CREATE STATISTICS" are not transferred. Our orders table holds county in lockstep with branch, and without that object the planner multiplies two independent guesses.

Rows the planner expects for one query, before and after pg_upgrade
Rows the planner expects for one query, before and after pg_upgradePlanner row estimates for a query on orders filtered by branch_id 7 and county_id 2, whose true answer is 4,000 rows. On PostgreSQL 14 with an extended statistics object the estimate was 3,980. Straight after pg_upgrade to 18, in copy, link or swap mode, it fell to 85, because statistics made with CREATE STATISTICS are not carried across. After vacuumdb with --missing-stats-only, which took 0.5 seconds, it was 3,989. An upgrade to 17 with --link carried no statistics at all and estimated 54.PostgreSQL 14before the upgrade3,98018, after pg_upgradecopy, link or swap8518, after vacuumdb--missing-stats-only, 0.5 s3,989COMPARE: 14 TO 1717, after pg_upgrade--link, no statistics54actual rows: 4,000
PostgreSQL 18 carries column statistics but not the ones made with CREATE STATISTICS, so the estimate fell from 3,980 rows to 85 until a half-second vacuumdb rebuilt them.

85 is the planner multiplying two separate guesses: 3,980 rows for the branch times about 1 in 47 for the county, because nothing left after the upgrade tells it that county follows branch.

On 17 it is worse: pg_upgrade --link to 17 brought no statistics at all, and the same query was planned for 54 rows. Either way the fix is the vacuumdb line pg_upgrade prints at the end. On 18 that is vacuumdb --all --analyze-in-stages --missing-stats-only, which took half a second here and put the estimate back to 3,989. Skip it and any query that leaned on an extended statistic is planned as if those columns were independent until something analyzes that table again. Ours expected 85 rows and got 4,000.

An Ubuntu shell prompt with sudo typed at it, where a PostgreSQL 14 to 18 upgrade on Ubuntu 22.04 begins

The order we run it in

  1. Inventory every cluster with pg_lsclusters and every extension in every database, and confirm a package exists for 18 for each one.
  2. Install postgresql-18 and the extension packages from PGDG, and decide checksums now: off with --no-data-checksums, or on with pg_checksums in a window long enough to rewrite the data.
  3. Restore last night's backup onto a spare server and run the real upgrade there, timed, the same rehearse-first order we set out for a move off MongoDB Atlas. It is the rehearsal and the backup test in one.
  4. On the night, take the backup, run pg_upgrade --check --link, then the upgrade with --link, or pg_upgradecluster -m link on Ubuntu.
  5. Start 18, run both vacuumdb lines from the summary, point the application at it and watch the slowest queries for an hour.
  6. Take a fresh backup of 18 and restore it once, then run delete_old_cluster.sh.

One database of a few gigabytes, no replica and a quiet weekend? Run those six steps yourself; this post is enough. The upgrades that hurt are the ones with streaming replicas, extensions nobody remembers installing, or an application that cannot be down for the length of a copy, and we handle those with you from the rehearsal to the day you delete the old cluster.

Send pg_lsclusters from each server, the extension list from each database, the data size, whether a replica follows the primary and how long the application can be down, through the database scoping form or on WhatsApp at +254 713 403 044. We send back the method, the order, the window it needs on your data and one figure for the upgrade. It is free and commits you to nothing.

WhatsApp