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 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.
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.
| Version | PostgreSQL 16 | PostgreSQL 17 | PostgreSQL 18 |
|---|---|---|---|
| Release date | September 14, 2023 | September 26, 2024 | end of September 2025 |
| VACUUM on large tables | Dead tuples in array, memory ceiling ≈ 1 GB | TidStore structure (radix tree), raised ceiling | Inherits the structure introduced in 17 |
| Input-output | Synchronous, block by block | Streaming I/O for ANALYZE and sequential scans | Generalized asynchronous I/O (AIO), configurable io_method |
| Concurrent Connections | Known contention on snapshot calculation | Reduced contention (GetSnapshotData optimized) | Inherits the gains introduced in 17 |
| Multi-column B-tree index | Complete scan if the head column is missing from the filter | Same as Postgres 16 | Skip scan: partial scan possible |
| Generated columns | STORED only | Same as Postgres 16 | Added VIRTUAL, becomes default behavior |
| Authentication | SCRAM, LDAP, certificates | Same 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.
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.
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.
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.
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.
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.
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.
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.
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.
What we get asked most often
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.