PostgreSQL 18 New Features for Performance
Key Takeaways:
- PostgreSQL 18 arrived on September 25, 2025 with an asynchronous I/O subsystem that the PostgreSQL Global Development Group reports has improved read performance up to 3 times, but the gains focus on sequential scans and cold-cache reads, not index scans.
- pganalyze tested a 3.5GB table on AWS io2 EBS dropping from 15,830 ms (PostgreSQL 17) to 5,723 ms with
io_method = io_uring, while PlanetScale’s 96-run benchmark foundio_uringoften performed worse than the defaultworkeron network storage.- The
io_workersdefault of 3 is too low for large machines; PostgreSQL contributor Tomas Vondra observed bitmap scans performing poorly at that setting and recommends roughly a quarter of available cores.uuidv7()generates timestamp-ordered identifiers that keep B-tree inserts sequential, and virtual generated columns are now the default, which changes what a migration from PostgreSQL 17 actually involves.- PostgreSQL 18.6 shipped August 13, 2026 alongside 17.11, 16.15, 15.19, and 14.24, fixing 28 security vulnerabilities; PostgreSQL 14 stops receiving fixes on November 12, 2026, and PostgreSQL 17 reaches end of life in November 2026.
What Shipped and Where 18.x Stands Now
PostgreSQL 18 was released on September 25, 2025, and the branch is now stable. The August 13, 2026 update shipped 18.6 alongside 17.11, 16.15, 15.19, and 14.24, fixing 28 security vulnerabilities across supported versions, according to the PostgreSQL security page. That release schedule matters for planning: if you are still deciding whether to upgrade, the question is no longer whether 18 is stable but whether your upgrade window closes before your current version reaches end of life.

Two deadlines are approaching. PostgreSQL 14 stops receiving fixes on November 12, 2026, and PostgreSQL 17 reaches end of life in November 2026, per the official versioning policy. Teams running 14 on long-lived appliances face a tighter deadline; teams on 17 have a more comfortable but limited window.
The release focuses less on new syntax and more on removing structural limits. The official release announcement highlights the asynchronous I/O subsystem, planner statistics that persist across major version upgrades, B-tree skip scan, uuidv7(), virtual generated columns, OAuth authentication, and RETURNING clauses that can now return both OLD and NEW values. There is also a less obvious change that will affect some operators: databases initialized with PostgreSQL 18 have page checksums enabled by default, and pg_upgrade requires matching checksum settings between clusters.
The Async I/O Subsystem: What It Actually Delivers
Before version 18, PostgreSQL performed blocking reads and depended on the operating system to mask latency. This approach performs poorly when each read takes milliseconds, which is typical on network-attached block storage. The new subsystem allows backends to queue multiple read requests instead of waiting for each to complete. The release announcement reports up to 3 times faster reads from storage, specifying that supported operations include sequential scans, bitmap heap scans, and vacuum. Writes and WAL operations remain synchronous.
Control is managed through a new io_method setting with three options. sync reproduces the old behavior. worker, the default, uses dedicated background I/O worker processes that pull requests from a shared queue and deliver data into shared buffers. io_uring submits reads directly through the Linux kernel interface of the same name, which requires building with --with-liburing and a compatible kernel. Changing this setting requires a server restart; it cannot be reloaded on the fly.
The tuning options are limited. io_workers defaults to 3, and the release notes increased the defaults for effective_io_concurrency and maintenance_io_concurrency from 1 to 16 to better suit modern hardware. The maximum read-ahead equals the product of effective_io_concurrency and io_combine_limit, which controls how aggressive prefetching is. PostgreSQL 18 also adds a pg_aios view that shows file handles in use for asynchronous I/O, plus per-backend I/O statistics.
One operational effect requires advance notice: asynchronous I/O changes how waits appear in monitoring. When reads block the backend, tools reliably report an I/O wait event. When reads are handled by workers, the backend’s wait profile changes, and existing dashboards may appear healthier even if the workload is not faster. Rebuild your I/O baselines after upgrading rather than comparing new numbers to old charts.
Independent Benchmarks: Where the Gains Are Real
The most relevant public measurement comes from pganalyze, who benchmarked a 3.5GB table on an AWS c7i.8xlarge instance with a 100GB io2 EBS volume provisioned at 20,000 IOPS, clearing the OS page cache between runs. On cold cache, PostgreSQL 17 took 15,830 ms for the scan. PostgreSQL 18 with io_method = sync took 15,071 ms, confirming the compatibility path is unchanged. The default worker mode took 10,051 ms, and io_uring took 5,723 ms. The full methodology and per-setting results are in the pganalyze async I/O deep dive.
PlanetScale ran a broader comparison: 96 combinations of PostgreSQL version, io_method, connection count, and scan size, against a roughly 300GB database on four EC2 configurations. Their results complicate the simple story. On network-attached storage, PostgreSQL 18 in sync and worker modes outperformed both PostgreSQL 17 and io_uring at single-connection load, and io_uring only pulled ahead on local NVMe with 50 connections and large scans. The PlanetScale benchmark also found that on EBS-backed instances, IOPS and throughput remained the main constraints, and the new I/O modes did not improve network storage as much as the headline suggests.
| Setup | Cold-cache result | Source |
|---|---|---|
| PostgreSQL 17, sync I/O | 15,830 ms for the scan | pganalyze |
PostgreSQL 18, io_method = sync |
15,071 ms, confirming unchanged behavior | pganalyze |
PostgreSQL 18, io_method = worker (3 workers) |
10,051 ms | pganalyze |
PostgreSQL 18, io_method = io_uring |
5,723 ms | pganalyze |
| PostgreSQL 18, on gp3 and io2 EBS, single connection | sync and worker beat io_uring |
PlanetScale |
| PostgreSQL 18, local NVMe, 50 connections, large scans | io_uring slightly ahead |
PlanetScale |
The two results align once you separate the variables. pganalyze tested a single large sequential scan on fast provisioned EBS, which is the workload io_uring handles best. PlanetScale tested mixed point-selects, range scans, and aggregations across many connections, where the worker pool parallelizes checksum verification and memory copies across processes. PostgreSQL contributor Tomas Vondra explains the mechanism in his AIO tuning writeup: with io_uring all the work happens inside the backend, so checksum verification and copying into shared buffers can become the bottleneck, while worker divides that work across processes.
UUIDv7 and the Primary Key Question
Random UUIDv4 primary keys scatter inserts across a B-tree, causing page splits and write amplification as tables grow. PostgreSQL 18 adds a native uuidv7() function that places a 48-bit millisecond timestamp at the start of the value, so generated identifiers sort by creation time. The implementation detail to note, documented in the Xata feature walkthrough, is that PostgreSQL stores a 12-bit sub-millisecond timestamp fraction in the space RFC 9562 calls rand_a and uses it as a counter, which keeps generated UUIDs monotonic within a backend even if the system clock moves backward.
The practical result is sequential inserts and less index fragmentation, with the coordination-free generation that makes UUIDs useful across services. PostgreSQL 18 also adds uuid_extract_timestamp() to extract the embedded time, and uuidv4() as an alias for gen_random_uuid().
The trade-off is storage and comparison cost. A UUID uses 16 bytes compared to 8 for bigint, and comparisons are more expensive. For a single-database application without external ID generation needs, an identity column remains the cheaper choice. UUIDv7 makes sense when identifiers must be generated outside the database, when merging data from multiple sources, or when you want IDs that do not reveal row counts and insertion order like sequential integers do.
Virtual Generated Columns and the Default That Changed
Generated columns have existed since PostgreSQL 12, but always as stored columns: computed on write and saved to disk. PostgreSQL 18 adds virtual generated columns, computed at read time with no storage cost, and makes virtual the default. The official generated columns documentation states clearly: “A generated column is by default of virtual kind.”
This change is likely to surprise teams during upgrade reviews, because a migration written without specifying storage mode now behaves differently than it did on PostgreSQL 17. A stored column can be indexed; a virtual column cannot, so any derived field you filter or sort on should be declared STORED explicitly. The documentation also lists restrictions that catch users: a virtual generated column cannot use a user-defined type, its expression must reference only built-in functions and types, it cannot reference another generated column, and it cannot be part of a partition key.
A subtler point from the Xata walkthrough is that virtual generated columns are stored in tuples as null values rather than omitted entirely. The column still consumes some space in the row layout, though much less than a materialized value. Logical replication of generated columns currently supports only stored ones, which matters if you use a change-data-capture pipeline.
The recommended migration practice is simple. Specify the storage mode in every generated column definition rather than relying on the default, so the read-cost versus write-cost decision is clear in code review instead of hidden in a version assumption.
The Upgrade Path and Its Gotchas
The main upgrade improvement is that pg_upgrade now preserves optimizer statistics across major version upgrades. Before 18, busy systems could experience query plan regressions after upgrade until ANALYZE repopulated statistics. PostgreSQL 18 also lets pg_upgrade run checks in parallel via --jobs and adds a --swap flag that swaps directories instead of copying, cloning, or linking files.
Several incompatibilities require attention before upgrading. Page checksums are enabled by default in new clusters, and pg_upgrade requires matching checksum settings, so upgrading a non-checksum cluster requires the --no-data-checksums option on initdb. Full-text search now uses the cluster’s default collation provider instead of always using libc, and the release notes recommend reindexing all full-text search and pg_trgm indexes after upgrading clusters whose default provider is ICU or builtin. MD5 password authentication is deprecated and emits warnings, with removal planned in a future major version; SCRAM replaces it. VACUUM and ANALYZE now process inheritance children by default, with the new ONLY option restoring the old behavior.
The upgrade sequence that avoids most issues is: inventory generated columns and decide their storage mode, confirm checksum settings match, plan the full-text and pg_trgm reindex, migrate authentication off MD5, and then run pg_upgrade against a staging copy with production-like data before upgrading the primary. For a broader comparison of PostgreSQL alongside embedded engines in the same architecture, our comparison of SQLite and PostgreSQL for production covers where each engine’s operational cost actually lies.
Troubleshooting in Production
Three failure modes appear repeatedly once 18 runs in production. The first is a bitmap scan that slows down instead of speeding up. Vondra’s benchmark found that with the default io_workers = 3, bitmap scans on low-selectivity queries performed poorly, and the worker method only improved performance once the worker count rose to 12. His advice is to increase io_workers to about a quarter of available cores. If bitmap heap scans regress after upgrading, that setting is the first to check.
The second is index scans that show no improvement. This is expected, not a misconfiguration. Index scans do not use the async I/O path yet, so all their I/O remains synchronous. If your workload mainly involves point lookups through indexes, the new I/O changes will have little effect, and the case for upgrading depends on other features.
The third is a checkpoint or write-path bottleneck unaffected by the upgrade. Because writes and WAL operations remain synchronous in 18, write-heavy systems should not expect read-side gains to translate. Write-heavy OLTP workloads benefit less than read-heavy analytical ones.
For diagnosis, PostgreSQL 18 provides more tools than previous releases. EXPLAIN ANALYZE now shows buffer counts by default and reports how many index lookups occur during an index scan. pg_stat_io reports read, write, and extend activity in bytes, and per-backend I/O and WAL statistics are available through pg_stat_get_backend_io() and pg_stat_get_backend_wal(). A new log_lock_failures setting logs SELECT ... NOWAIT lock acquisition failures, making contention visible without instrumenting the application.
Trade-offs and Limits
The async I/O subsystem improves read performance, and that distinction matters. Sequential scans, bitmap heap scans, and vacuum benefit; index scans do not; writes and WAL do not. Workloads dominated by short indexed lookups and heavy writes may see little benefit from this feature.
Configuration is not one-size-fits-all. pganalyze recommends io_uring for maximum read throughput on their test setup, while Vondra’s benchmark found it significantly slower than worker for sequential scans on his hardware, and PlanetScale found it losing to worker on network-attached storage. Vondra also notes that some container runtimes disable io_uring support for security reasons, which can make it unavailable even on recent kernels. There is no single best io_method, and the only reliable answer comes from benchmarking your own workload on your own storage.
A broader caution predates this release. An independent assessment of hundreds of production PostgreSQL deployments, published at Gold Lapel, found recurring issues: missing indexes, N+1 query patterns from ORMs, and autovacuum configurations unchanged since provisioning. Its example is a 12.4 million row table where a filtered query scanned every row in 847 ms, and adding one composite index dropped it to 0.4 ms. No I/O subsystem change competes with that, and no upgrade fixes a query that was never indexed. PostgreSQL 18 removes real limits on the storage path; it does not change the queries you send it.
Sources and References
Sources cited while researching and writing this article:
- Waiting for Postgres 18: Accelerating Disk Reads with Asynchronous I/O
- Benchmarking Postgres 17 vs 18 , PlanetScale
- Tuning AIO in PostgreSQL 18 – Tomas Vondra
- PostgreSQL security page
- official versioning policy
- PostgreSQL: PostgreSQL 18 Released!
- Postgres 18 Features: Async I/O, UUIDv7, OAuth and More | xata
- PostgreSQL: Documentation: 18: 5.4. Generated Columns
- The State of PostgreSQL Performance, 2026: A Thorough Assessment | Gold Lapel
Thomas A. Anderson
Mass-produced in late 2022, upgraded frequently. Has opinions about Kubernetes that he formed in roughly 0.3 seconds. Occasionally flops, but don't we all? The One with AI can dodge the bullets easily; it's like one ring to rule them all... sort of...
