Dev & EngARTICLE

PostgreSQL 18 turns one year old: here's how to test asynchronous I/O before going to production

Released in September 2025, Postgres 18 brought an asynchronous I/O subsystem that directly targets the disk bottleneck in sequential scans, bitmap heap scans, and vacuum. Here's how io_method works and how to validate each mode before switching in production.

PostgreSQL 18 was released on September 25, 2025, according to the project's official release notes. It's been exactly one year, and the community is already discussing PostgreSQL 19 (version 19 Beta 4 came out on September 24, 2026). But for those who haven't migrated yet, or migrated and never touched the new parameters, it's worth revisiting the item that matters most for anyone dealing with I/O in production: the asynchronous I/O subsystem (AIO), controlled by the io_method parameter.

Before Postgres 18, every backend that needed to read a page from disk did so synchronously: it requested the block, blocked, waited for the kernel to respond, and moved on. In sequential scan, bitmap heap scan, and vacuum, this meant a queue of read requests processed one at a time, even when the underlying storage (NVMe SSD, EBS, RAID disk) could easily handle several parallel requests. The classic result: idle CPU while the process waits on I/O, and vacuum that takes hours on storage that technically has bandwidth to spare.

What AIO Actually Changes

According to the release notes, the new subsystem "allows backends to queue multiple read requests," which makes sequential scans, bitmap heap scans, and vacuums more efficient. This is different from query parallelism (which already existed): here the gain happens within a single process, which can now fire off several disk reads before needing the first one to finish, instead of serializing everything.

The most visible practical change for database administrators: the effective_io_concurrency and maintenance_io_concurrency parameters now also apply on systems without fadvise() support, and the default value for both went up to 16 (it was more conservative before). This already reflects the expectation of modern hardware with a deeper I/O queue than a 2010 rotational disk.

The Three io_method Modes and Their Trade-offs

AIO behavior is controlled by the io_method GUC. The release notes confirm that the parameter exists and that it, together with io_combine_limit and io_max_combine_limit, controls how the asynchronous subsystem queues reads, but they don't detail which values the parameter accepts, what the factory default is, or the specific operating system or compilation requirements for each option. Before changing this parameter in production, it's worth checking your version's configuration documentation to confirm these points: each value has different portability and performance implications, and picking the wrong value may not deliver the expected gain (or may not even work, depending on the environment).

There's no universal "right" mode: the choice depends on the operating system, the kernel version, and how the Postgres binary was compiled, so it's worth testing more than one option on your real workload before deciding which one goes to production.

How to Test Before Switching in Production

Editor's note: the code in this section was not executed in a real environment. Validate it before using it in production.

The recommended approach is to isolate the change in a staging environment with real load (or as close to it as possible) before touching the production cluster:

sql
-- check the current mode
SHOW io_method;

-- change it (check the parameter's configuration documentation to confirm
-- the accepted values and whether the change requires a server restart;
-- the release notes used in this report don't list the valid values)
ALTER SYSTEM SET io_method = '<value confirmed in your version's documentation>';

After restarting, Postgres 18 exposes a new view, pg_aios, that shows the file handles in use by the asynchronous subsystem:

sql
SELECT * FROM pg_aios;

To see the effect in data volume, the most direct way is to look at pg_stat_io, which gained new columns in this version: read_bytes, write_bytes, and extend_bytes (the old op_bytes column, which always equaled BLCKSZ, was removed). Running a VACUUM VERBOSE on a large table and comparing total time across different io_method values gives a concrete indicator of gain before applying it in production, along with the new timing columns in pg_stat_all_tables (total_vacuum_time, total_autovacuum_time, total_analyze_time, total_autoanalyze_time), which help measure whether vacuum actually got faster without needing any external instrumentation.

It's also worth adjusting io_combine_limit and io_max_combine_limit, the two parameters that control how many reads Postgres tries to group into a single, larger I/O request. Increasing these limits tends to help on storage with good sequential bandwidth (NVMe SSD), but on storage with high per-operation latency (some network scenarios, such as certain cloud network volumes) the gain may be marginal or even negative if the combining gets too aggressive for the workload's access pattern.

When It's Not Worth Changing Right Now

If the cluster is already running with the factory-default io_method and the application doesn't feel a noticeable disk bottleneck, changing this parameter without measuring first is an unnecessary risk. Before switching values, it's worth checking the parameter's official documentation for which options exist, what the default is for your version, and each one's operating system or compilation requirements; the release notes used in this report don't detail these points. It's only worth investigating alternatives if the workload has enough sequential read volume or heavy vacuum activity for the gain to show up in pg_stat_io metrics. On small databases, with a working set that fits entirely in shared_buffers, AIO simply doesn't have relevant disk I/O to speed up, and the effort of validating a different io_method isn't worth it.

The practical takeaway: anyone who's kept Postgres in production for years has learned to distrust any new GUC that promises free performance. Postgres 18's AIO is a real architectural change, not a cosmetic tweak, and it deserves the same treatment as any storage engine change: measure in staging with pg_aios and pg_stat_io enabled, compare the available modes under your system's real workload, and only then decide which io_method goes into the production postgresql.conf.

Translated from the Brazilian Portuguese original · Read the original