· via dev.to (home feed)
PostgreSQL 18 async I/O runs on by default — guide details the settings worth testing
A guide on dev.to walks through PostgreSQL 18's asynchronous I/O subsystem, which is active with default settings after upgrade, and outlines which parameters operators should test, one change at a time.

Async I/O is already running
PostgreSQL 18 introduced an asynchronous I/O subsystem, and because it ships enabled with its default settings, an upgraded server is already using it whether or not anyone tuned it. That framing opens a practical guide by Oleksandr Kuryzhev published on dev.to (originally on kuryzhev.cloud), which argues the useful question is not whether to enable the feature but which few settings deserve a controlled test on real data.
What the subsystem covers
Before 18, a backend asked the kernel for a block and waited; read-ahead and posix_fadvise hints helped in places, but reads stayed synchronous from the backend's point of view. The new subsystem lets a backend submit several read requests and keep working while they are in flight.
Two things decide how that happens. The io_method setting picks the mechanism: worker, the default, hands reads to dedicated I/O worker processes; io_uring uses the Linux kernel's ring interface directly from the backend; sync keeps the old blocking behavior. The second factor is which code paths use it. According to the article, the main beneficiaries are sequential scans, bitmap heap scans and vacuum, while writes and WAL are not part of the change, so write-heavy OLTP gains little.
Related defaults also moved in 18: effective_io_concurrency and maintenance_io_concurrency were raised, so an upgraded server may behave differently before anyone touches a setting.
The short recommendation
The author's rule of thumb is to keep io_method = worker unless two conditions hold: you are on Linux with a kernel and PostgreSQL build (compiled with liburing support) that enable io_uring, and your queries are read-heavy with cold or large working sets. If both hold, A/B test worker against io_uring, then io_workers, then effective_io_concurrency, one change at a time. If hot data fits in shared_buffers and the OS cache, expect little or nothing.
Common failure modes
The guide lists four recurring mistakes. Copying a tuning snippet and restarting production with several changed values leaves nobody knowing which setting helped, hurt or did nothing; one plausible regression is oversized worker counts competing with query backends for CPU. Benchmarking on a warm cache makes every method look identical because there is no read I/O to overlap, while a single dropped-caches run can be mistaken for steady-state behavior on a mostly cached workload. Assuming io_uring is simply the fast option ignores that it depends on kernel support, a liburing-enabled build and a runtime that permits the syscalls — default seccomp profiles and hardened hosts often restrict io_uring, so a setting that works on a VM can fail inside a container. Finally, expecting gains on selective index lookups, write-heavy ingestion or WAL-bound workloads misses that 18's subsystem does not target those paths.
How to test properly
The recommended procedure starts with a baseline: query pg_settings for the current values of io_method, io_workers, io_max_concurrency, io_combine_limit, effective_io_concurrency, maintenance_io_concurrency and track_io_timing, and confirm io worker processes exist in pg_stat_activity. Enable track_io_timing so numbers show up in EXPLAIN output and pg_stat_io, and stage candidate changes in a drop-in config file for easy reversion. io_method and io_max_concurrency need a restart, while io_workers can be changed with a reload.
Testing belongs on a non-production host with a cold cache — restart PostgreSQL and clear the OS page cache — running the same queries with EXPLAIN (ANALYZE, BUFFERS, SETTINGS), repeating each case several times and comparing medians rather than the best run. Notably, if io_uring cannot be used, the server refuses to start rather than falling back silently, either because the build lacks liburing or because the kernel or seccomp blocks the syscalls.
Concurrency, storage and observability
Once a method is chosen, tune concurrency against the storage. Low-latency local NVMe often reaches its useful limit at lower concurrency than network storage, while cloud block-store volumes reward more outstanding requests up to their IOPS or throughput cap — that cap, not PostgreSQL, may be the real ceiling. For worker mode, size io_workers against CPU headroom, since each worker is a process competing with backends. Vacuum reads are governed by maintenance_io_concurrency, making a bloated-table vacuum on a staging copy a clean, isolated test.
For live observation, the pg_aios view shows in-flight I/O (it requires superuser or pg_read_all_stats), and pg_stat_io reports read volume and time per backend type. The article cautions that 18 added byte columns such as read_bytes, so monitoring queries should be verified against the PostgreSQL 18 documentation.
Why it matters
Async I/O is a performance feature that activates implicitly during a routine major-version upgrade. Operators who skip tuning still get new behavior — new defaults, new background processes, changed I/O patterns — so a before/after comparison on identical queries is the real deliverable. The guide's discipline of one variable at a time, cold caches and median results is what turns a headline about faster scans into evidence, and it protects production systems from ticket-driven changes nobody can attribute. Expect gains concentrated in large sequential scans over uncached data; CPU-bound queries and point lookups will not move.
- #postgresql
- #postgresql-18
- #performance-tuning
- #databases
- #asynchronous-io