PostgreSQL EOL: Version Support Timeline
Chat2DB TeamRunning an unsupported database is one of those risks that costs nothing right up until it costs everything. There is no outage on the day your PostgreSQL version reaches end of life. The server keeps serving queries exactly as it did the day before. What changes is that the next security vulnerability discovered in that code path will never be patched for you, and the clock on that starts ticking immediately.
This guide covers how the PostgreSQL support policy actually works, when each version goes out of support, how to find out what you are running, and how to plan the upgrade.
How the PostgreSQL support policy works
The PostgreSQL Global Development Group supports each major version for five years after its initial release. After that date the version is considered end of life (EOL) and receives no further fixes of any kind — including security fixes.
Three details matter more than people expect:
Major versions are the unit of support. Since PostgreSQL 10, the first number is the major version. PostgreSQL 16.2 and 16.9 are the same major version, 16, receiving minor releases. Before version 10 the scheme was different: 9.5 and 9.6 were different major versions, which is why you sometimes see upgrade guides that look inconsistent.
Minor releases ship quarterly and are cumulative. They contain bug fixes and security patches only — never new features and never a change to the on-disk format. That is what makes them safe: you stop the server, install the new binaries, start it again. There is no dump and restore, no pg_upgrade, no rewrite. If you are on a supported major version but have not applied minor releases in a year, you are carrying known, published, already-fixed vulnerabilities for no reason.
EOL happens in November. Final minor releases for a version that is going EOL are published alongside the regular November minor release. A version released in September or October of a given year therefore reaches EOL in the November five years later.
The version timeline
Applying that five-year rule to each major release gives the following schedule:
| Major version | Released | End of life |
|---|---|---|
| PostgreSQL 10 | October 2017 | November 2022 |
| PostgreSQL 11 | October 2018 | November 2023 |
| PostgreSQL 12 | October 2019 | November 2024 |
| PostgreSQL 13 | September 2020 | November 2025 |
| PostgreSQL 14 | September 2021 | November 2026 |
| PostgreSQL 15 | October 2022 | November 2027 |
| PostgreSQL 16 | September 2023 | November 2028 |
| PostgreSQL 17 | September 2024 | November 2029 |
| PostgreSQL 18 | September 2025 | November 2030 |
As of late 2026, that means PostgreSQL 13 and everything below it is already out of support, and PostgreSQL 14 has weeks left. If you are on 14, the upgrade is not a next-quarter problem.
Dates for future versions follow the same policy, but always confirm against the official versioning page on postgresql.org before you build a compliance document around them — the project has occasionally adjusted a final release date.
Finding out what you are actually running
The first surprise in most audits is that nobody is certain. Start here:
-- Full version string, including the build and platform
SELECT version();
-- Just the number, as an integer: 160004 means 16.0.4
SHOW server_version_num;
-- Human-readable
SHOW server_version;server_version_num is the one to use in scripts, because comparing it is trivial:
SELECT
current_setting('server_version_num')::int AS version_num,
current_setting('server_version') AS version,
CASE
WHEN current_setting('server_version_num')::int < 140000
THEN 'END OF LIFE — upgrade now'
WHEN current_setting('server_version_num')::int < 150000
THEN 'EOL November 2026 — plan the upgrade'
ELSE 'Supported'
END AS support_status;From the shell, without connecting:
psql --version # the client version, not the server
postgres --version # the server binary
psql -c "SELECT version();" # what the server actually reportsThose first two commands catch a classic confusion: psql --version tells you about the client on your laptop, which frequently differs from the server you are connected to. Always check the server.
If you manage many instances, a client that lists them side by side saves real time. Chat2DB (opens in a new tab) shows the server version for every connection in one place, so an audit across a dozen databases is a glance rather than a dozen sessions.
Checking your extensions too
An upgrade is rarely blocked by PostgreSQL itself. It is blocked by an extension that has not been rebuilt for the new major version. Inventory them before you plan anything:
SELECT
e.extname AS extension,
e.extversion AS installed_version,
n.nspname AS schema,
(SELECT max(version)
FROM pg_available_extension_versions v
WHERE v.name = e.extname) AS latest_available
FROM pg_extension e
JOIN pg_namespace n ON n.oid = e.extnamespace
ORDER BY e.extname;Anything from outside core — PostGIS, TimescaleDB, pgvector, pg_partman, Citus — needs a compatible build installed on the new server before you start. Each of these also has its own upgrade procedure that may need to run before or after the PostgreSQL upgrade; PostGIS in particular requires care.
Choosing an upgrade path
There are three realistic approaches, and the right one depends almost entirely on how much downtime you can accept.
pg_upgrade with hard links
The standard choice for most teams. pg_upgrade migrates the system catalogues to the new version's format and, with --link, uses hard links for the data files instead of copying them. That makes the runtime largely independent of database size — minutes rather than hours.
# Always run the check first. It is read-only and lists every blocker.
pg_upgrade \
--old-datadir=/var/lib/postgresql/14/main \
--new-datadir=/var/lib/postgresql/17/main \
--old-bindir=/usr/lib/postgresql/14/bin \
--new-bindir=/usr/lib/postgresql/17/bin \
--check
# Then the real run
pg_upgrade \
--old-datadir=/var/lib/postgresql/14/main \
--new-datadir=/var/lib/postgresql/17/main \
--old-bindir=/usr/lib/postgresql/14/bin \
--new-bindir=/usr/lib/postgresql/17/bin \
--link --jobs=4The critical caveat: with --link, the old cluster is not usable once the new one has started. The two clusters share the same files. Your rollback plan cannot be "start the old one again" — it has to be a restored backup or a replica. If you want a cheap rollback, drop --link and accept the copy time.
Afterwards, statistics are not carried over, so the planner is flying blind until you rebuild them:
vacuumdb --all --analyze-in-stages --jobs=4Run this before you send production traffic. --analyze-in-stages does three increasingly accurate passes so you get rough statistics quickly rather than waiting for a full analyze.
Logical replication
The near-zero-downtime option. Build the new-version server, replicate into it while the old one serves traffic, then cut over when the lag reaches zero.
On the publisher (old server, wal_level = logical):
CREATE PUBLICATION upgrade_pub FOR ALL TABLES;On the subscriber (new server):
CREATE SUBSCRIPTION upgrade_sub
CONNECTION 'host=old-db port=5432 dbname=app user=replicator password=...'
PUBLICATION upgrade_pub;Watch the lag, and only cut over when it is trivial:
SELECT
slot_name,
active,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)
) AS lag
FROM pg_replication_slots;The cost of this approach is the constraints logical replication carries. It does not replicate schema changes, sequence values, or large objects, and every table needs a replica identity — a primary key, or an explicitly configured unique index. Sequences are the one that bites people: they must be advanced manually on the new server before it accepts writes, or the first insert collides with existing rows.
-- Run on the OLD server, then apply the output on the new one
SELECT 'SELECT setval(' || quote_literal(schemaname || '.' || sequencename)
|| ', ' || last_value || ', true);'
FROM pg_sequences
WHERE last_value IS NOT NULL;Dump and restore
The simplest to reason about and the slowest to run. Downtime scales with data size, so it is realistic for small databases only — but it is also the most thorough, because it rebuilds everything from scratch.
pg_dump --format=directory --jobs=8 --file=/backup/app app
pg_restore --dbname=app --jobs=8 /backup/appAlways dump with the new version's pg_dump. It understands the old server and emits SQL the new server wants; the reverse is not guaranteed.
Before you upgrade: read the incompatibilities
Every major release has a "Migration to Version N" section at the top of its release notes listing changes that can break existing applications. Read the section for every version you are skipping, not just the target. Going from 14 to 17 means reading the notes for 15, 16 and 17.
Historically these have included things like the removal of the password_encryption = md5 default path, changes to pg_stat_statements column names, and the removal of long-deprecated functions. None are dramatic on their own; all of them are cheaper to find in release notes than in production.
A practical pre-flight sequence:
- Restore a production backup onto a new-version server in a staging environment.
- Run
pg_upgrade --checkand resolve everything it reports. - Run your application's full test suite against it.
- Replay a sample of real production queries and compare plans —
EXPLAINoutput can change between majors, occasionally for the worse. - Confirm every extension has a compatible build installed.
- Time the whole thing, so your maintenance window is based on measurement rather than hope.
What to do if you are already past EOL
If you are on PostgreSQL 13 or older today, you are unsupported. The pragmatic sequence:
First, get current on minor releases within your existing major version. If you are on 13.4, moving to the final 13.x release is a binary swap with no data migration, and it closes every vulnerability patched in between. Do this in days, not months — it buys you safety while you plan the real upgrade.
Then upgrade to a supported major version — and skip ahead. pg_upgrade supports jumping multiple majors in one step. Going from 13 straight to 17 is one maintenance window, not four. Since the work is dominated by testing rather than by the upgrade itself, landing on a recent version maximises the time before you have to do it again.
Consider commercial extended support only as a bridge. Several vendors offer patches for EOL versions. It is a legitimate option when a hard compliance deadline outruns your engineering capacity, but it is a way to buy time for the upgrade rather than a substitute for it.
Build the check into your routine
The reason databases end up years out of support is never a decision — it is the absence of one. Make the version a thing you look at:
-- Drop this into a monthly review, a dashboard, or a monitoring check
SELECT
current_setting('server_version') AS version,
current_setting('server_version_num')::int AS version_num,
pg_postmaster_start_time() AS running_since,
current_setting('server_version_num')::int / 10000 AS major;Two habits cover most of the risk: apply minor releases every quarter, and schedule the major upgrade when the version has about a year of support left rather than a month. That turns an emergency into routine maintenance.
If you are running several PostgreSQL instances, keeping the version, extensions and configuration of each one visible in a single client makes these audits far less tedious. You can do that with Chat2DB (opens in a new tab), or try it in the browser at app.chat2db.ai (opens in a new tab).
Summary
PostgreSQL supports each major version for five years, with EOL landing in November. PostgreSQL 13 and older are already unsupported; PostgreSQL 14 reaches end of life in November 2026. Check what you run with SHOW server_version_num, inventory your extensions before planning anything, and pick an upgrade path based on your downtime budget: pg_upgrade --link for a short window, logical replication for near-zero downtime, dump and restore for small databases. Read the migration notes for every version you skip, rebuild statistics with vacuumdb --analyze-in-stages before taking traffic, and upgrade while you still have a year of support left rather than a month.
