PRODSovereign European BaaS platformOpen Dashboard →

Performance · 9 min read

Postgres 16 vs 17 vs 18: gains that matter

Affane Daylami · Fondateur · May 27, 2026

Back to blog

Postgres 17 is not "faster" than Postgres 16 on a vague set of queries. The real gain comes from two specific areas: the memory consumed by VACUUM on large tables, and the contention on connections with high competition. Postgres 18, released at the end of 2025, adds an even more profound change: asynchronous input-output. Here is what these versions really change, with their sources, and why Aurabase is still running Postgres 16 in production today despite this.

This English text was generated automatically from the French original and has not been reviewed yet.

This article draws on the official PostgreSQL project documentation and two technical analyzes published after each major release, Microsoft Tech Community (Azure Database for PostgreSQL team) and Crunchy Data. No figure below is a benchmark reproduced by us: when data comes from a third party, we indicate it, with its source and date. For the methodology we apply to our own measurements, see our benchmark methodology pillar.

The essentials
  • The main gain of Postgres 17 is the memory overhaul of VACUUM (TidStore structure), which removes the old cap of around 1 GB. The official release notes indicate up to 20x less memory used in some cases.
  • Postgres 17 also reduces the contention on calculating transaction snapshots, which especially benefits high concurrency instances on multi-core hardware.
  • Postgres 18 (late September 2025) introduces asynchronous I/O (AIO), the most structural architectural change in several major releases, particularly for high-latency storage.
  • Postgres 18 also adds skip scanning on multi-column B-tree indexes, virtual generated columns by default, and OAuth 2.0 support for authentication.
  • Aurabase is running Postgres 16.15 in production today, verified in the code: not a delay, a documented choice linked to the irreversibility of major version upgrades under CloudNativePG.
#
Overview

What really changes between Postgres 16, 17 and 18

The three versions are not distinguished by a single overall performance figure. Each corrects a specific point of the architecture, with a different audience each time: large tables for Postgres 17, high latency storage for Postgres 18. The table below summarizes the verifiable facts, all dated, before going into the details of each project.

VersionPostgreSQL 16PostgreSQL 17PostgreSQL 18
Release dateSeptember 14, 2023September 26, 2024end of September 2025
VACUUM on large tablesDead tuples in array, memory ceiling ≈ 1 GBTidStore structure (radix tree), raised ceilingInherits the structure introduced in 17
Input-outputSynchronous, block by blockStreaming I/O for ANALYZE and sequential scansGeneralized asynchronous I/O (AIO), configurable io_method
Concurrent ConnectionsKnown contention on snapshot calculationReduced contention (GetSnapshotData optimized)Inherits the gains introduced in 17
Multi-column B-tree indexComplete scan if the head column is missing from the filterSame as Postgres 16Skip scan: partial scan possible
Generated columnsSTORED onlySame as Postgres 16Added VIRTUAL, becomes default behavior
AuthenticationSCRAM, LDAP, certificatesSame as Postgres 16+ OAuth 2.0 (RFC 8628, device flow)

Sources: official release notes of the PostgreSQL project (postgresql.org), cross-referenced with analyzes published by Microsoft Tech Community and Crunchy Data after each major release. Accessed August 24, 2026.

#
Postgres 17

VACUUM's memory overhaul is a game-changer for big tables

Before Postgres 17, VACUUM stored the list of dead tuples to clean up in a simple array, sized by maintenance_work_mem. The problem was not the speed of the calculation, but the structure itself: this table plateaued around 1 GB, no matter how much was configured beyond that. On a table with more than about 178 million dead rows, VACUUM had to loop in multiple passes, each rereading the entire indexes.

Postgres 17 replaces this array with a structure called TidStore, an adaptive radix tree that heavily compresses the space needed to store tuple identifiers. The project's official release notes indicate a reduction in memory used by VACUUM of up to 20 times in some cases, without the artificial ceiling associated with the old structure. Source: PostgreSQL 17 official release notes, postgresql.org, September 26, 2024. Microsoft Tech Community and Crunchy Data each published a technical analysis of this change shortly after release. Both confirm the concrete interest for tables of several hundred million rows, with a high deletion or update rate.

×20
Less VACUUM memory
measured cases, PostgreSQL 17 release notes
≈1 GB
Old memory ceiling
dead tuple array structure, Postgres ≤16
16.15
Version supporting Aurabase
checked in code on August 24, 2026

This project mainly benefits a specific scenario: a large table with a high deletion or update rate. VACUUM previously ran in several passes due to lack of available memory. On a small table, or on a predominantly reading load, the gain remains marginal, or even invisible.

#
Postgres 17

Less contention on high concurrency connections

The second Postgres 17 project touches on a more discreet point: the calculation of transaction snapshots. Each query needs to know what other transactions are in progress to enforce Postgres' MVCC visibility rules. On a machine with a high number of cores and active connections, this calculation generated contention on a shared internal structure. This is a bottleneck that has been documented for a long time by project contributors.

Postgres 17 reduces this contention. The effect is mainly measured on high concurrency instances, with many simultaneous active connections on multi-core hardware. On a low concurrency load, the difference with Postgres 16 remains marginal: it is a scalability project, not a reduction in latency per isolated request.

This gain does not replace a connection pooler, it simply reduces its internal cost. If the number of active connections is already your bottleneck, the major version takes a back seat. Our max_connections tuning guide and our PgBouncer transaction mode comparison explore this subject in more detail.

#
Postgres 18

Asynchronous I/O: the most profound architectural change in years

Postgres 18, released at the end of September 2025, addresses a more structural problem. Until then, every Postgres disk read blocked the process that requested it. The new asynchronous input-output (AIO) subsystem allows a process to initiate multiple reads in parallel and continue working while they complete, instead of waiting for each one sequentially.

The io_method parameter controls this behavior: worker (processes dedicated to I/O, default) or io_uring on Linux, when Postgres was compiled with this support. Sequential scans, bitmap heap scans and VACUUM are the first beneficiaries, particularly on high latency storage: network disks, cloud volumes, rather than local NVMe.

PlanetScale, which offers a managed Postgres offering, has published its own Postgres 17 vs 18 comparisons focused on this I/O change. These are their measurements on their own infrastructure, not figures that we reproduced independently here. Take it as a signal that the subject is worth testing on your actual load, not as a universal percentage.

The generalized AIO of Postgres 18 continues a project started in Postgres 17, not an isolated change. Version 17 had already introduced the streaming I/O interface, but limited to ANALYZE and sequential scans. Postgres 18 extends this same logic to a broader scope of operations, including VACUUM and bitmap heap scans. The two versions therefore read as a progression, not as two separate bets on I/O.

#
Postgres 18

Other changes that matter

Three other Postgres 18 changes are worth monitoring, even if they don't directly address raw performance.

The skip scan on multi-column B-tree index allows Postgres to use a composite index even when the query does not filter on its head column. Before Postgres 18, this scenario often required a complete scan of the table, or the creation of an additional dedicated index.

The generated virtual columns (GENERATED ALWAYS AS (...) VIRTUAL) become the default behavior when STORED is not specified. A virtual column is calculated on read rather than being written to disk, which reduces the amount written each time the source row is inserted or updated.

Postgres 18 finally adds support for OAuth 2.0 on the authentication side (RFC 8628, device flow), alongside existing mechanisms like SCRAM, certificates or LDAP. A relevant point for any organization that already centralizes its identities via an external OAuth/OIDC provider.

#
Real case

Why Aurabase still runs on Postgres 16, and what would change this choice

At Aurabase, the tenant database currently runs in Postgres 16.15, not 17. This can be verified directly in the repository: the reference CNPG image (docker/Postgres.CNPG.Dockerfile) starts from ghcr.io/cloudnative-pg/postgresql:16-standard-bookworm, pinned by digest, the same major version as the shared tier (docker/Postgres.Dockerfile). Verified August 24, 2026.

The code also documents why. A correction comment in k8s_tenant.rs explains that an earlier fallback mistakenly pointed to postgresql:17.2. The reason given at the time, the standard image would not include pgvector, turned out to be false upon verification. Both images have pgvector: 0.8.0 on 17.2, 0.8.5 on 16-standard-bookworm measured on the fleet cluster.

The real risk, documented in the comment itself, is elsewhere: CloudNativePG prohibits any major version downgrade once a Cluster has been created. A fleet provisioned by mistake in Postgres 17 would be irreversible, whereas everything that was validated end-to-end at Aurabase was in Postgres 16.

k8s_tenant.rs (simplified extract)rust
// Major version retained if TENANT_POSTGRES_IMAGE is not defined
let repli = "ghcr.io/cloudnative-pg/postgresql:16-standard-bookworm";

tracing::warn!(
    image = repli,
    "TENANT_POSTGRES_IMAGE non défini : repli sur la version validée."
);

This is not a judgment on Postgres 17 as such. It is a policy of operational prudence: do not switch a production fleet to a major version until end-to-end validation has followed. The same reasoning applies to any team that manages Postgres via CloudNativePG or an equivalent Kubernetes operator. The question is not only the expected performance gain, it's also the path back if something goes wrong.

No rollback on a major version

PostgreSQL does not offer major version downgrades. pg_upgrade migrates in one direction only, and CloudNativePG applies the same constraint at the level of its operator. The only way back is to restore a backup from before the update, or start from a new instance in the old version.

Should you migrate to Postgres 17 or 18 now?

Three criteria make it possible to decide without waiting for a universal figure. First, the size and mutation rate of your largest tables: if VACUUM is already running in several passes, the Postgres 17 memory worksite applies directly to your case. Then, your storage: on low latency local SSD, the asynchronous I/O of Postgres 18 provides less than on a network volume. Finally, your way back: on an operator that prohibits major downgrade, testing first on a disposable environment is not an optional precaution.

Concretely, the same rule applies to any fleet managed by a Kubernetes operator. First provision a test cluster in the target release, then replay a load representative of your production on it. Only touch the actual cluster once this test has been validated end-to-end, not just reading the release notes. If your decision also concerns the choice between dedicated base and shared base to absorb this type of change, our article dedicated vs shared base explores this angle.

#
Frequently asked questions

What we get asked most often

Is Postgres 17 faster than Postgres 16 in general use?+
Not uniformly. The concrete gain focuses on two specific points: the memory used by VACUUM on large tables, and contention on high concurrency connections. On a light load with small tables, the difference remains barely perceptible.
Can we go back from Postgres 17 or 18 to Postgres 16 after an upgrade?+
No, not directly. PostgreSQL does not offer a major version downgrade once the upgrade is complete: pg_upgrade migrates in one direction only. The only possible return is to restore a backup prior to the update, or start from a new instance in the old version.
Is Postgres 18 asynchronous I/O enabled by default?+
The subsystem exists by default, but with io_method=worker (processes dedicated to I/O), not io_uring. io_uring remains an option on Linux, to be activated explicitly when Postgres has been compiled with this support.
What version of Postgres does Aurabase use today?+
Postgres 16.15, two-thirds (shared base and dedicated base per project), verified in docker/Postgres.Dockerfile and docker/Postgres.CNPG.Dockerfile on August 24, 2026. This is not a permanent cap, only the current end-to-end validated state.
#
In summary

Performance matters less than reversibility

The choice between Postgres 16, 17 and 18 is not just about which version is “fastest”. Postgres 17 fixes a real structural VACUUM problem on large tables and reduces contention at high concurrency. Postgres 18 goes further with asynchronous I/O, an architectural change that requires testing on your actual load and storage before being generalized.

The criterion most often forgotten is not performance, it is reversibility. On an operator like CloudNativePG, a major version upgrade is not undone after the fact. Before switching over a production fleet, the real question is not only the expected gain, it is also the return path if the test fails. If you are preparing for this version upgrade, our Postgres tuning checklist in production details the settings to revalidate after a major version change.

READY TO DEPLOY?

Your backend in five minutes.

No credit card required · 500 MB free · 50,000 MAU