PostgreSQL 19 asynchronous I/O - worker autoscaling and read-ahead
Asynchronous I/O arrived in PostgreSQL 18: instead of a backend issuing one read and blocking on
it, scans submit a stream of prefetch requests that the kernel or a pool of I/O workers services in
parallel. 18 shipped it with a fixed pool - io_workers workers, no more, no fewer, whatever the
load. 19 makes the pool elastic, improves how large requests are scheduled, and adds the EXPLAIN
output that lets you see the machinery.
The autoscaling worker pool
io_method still defaults to worker, but the fixed io_workers count is gone. Four new GUCs
govern the pool:
| GUC | Default | What it does |
|---|---|---|
io_min_workers |
2 |
Floor of the I/O worker pool |
io_max_workers |
8 |
Ceiling of the I/O worker pool |
io_worker_idle_timeout |
1min |
Idle workers are retired after this |
io_worker_launch_interval |
100ms |
New workers cannot be launched more often than this |
The shape of the trade-off is the classic autoscaler: io_min_workers guarantees a warm pool so
the first prefetch burst does not wait on process launch; io_max_workers caps how much process
and memory overhead a busy cluster spends on I/O; io_worker_launch_interval stops a spiky
workload from thrashing the process table. On a host that shared its io_workers between queries
and vacuum, the ceiling now decides how hard vacuum’s streaming reads can push in parallel.
Text equivalent
- Scan nodes submit read-ahead requests to the I/O queue instead of blocking on each read.
- The worker pool drains the queue, scaling between
io_min_workers(2) andio_max_workers(8). - Idle workers retire after
io_worker_idle_timeout(1min); launches throttle toio_worker_launch_interval(100ms). - The
EXPLAINIOoption shows the queue depth, request counts, waits and concurrency per scan.
The release notes also list improved read-ahead scheduling for large requests - the scheduler
that decides how far ahead of the scan prefetch runs. The beta documentation does not expose a knob
for it; it is a correctness-of-throughput change you observe through EXPLAIN (ANALYZE, IO) rather
than tune.
Seeing it: the EXPLAIN IO option
EXPLAIN gains an IO option that reports, per scan node that provides the information: the
prefetch queue’s average and maximum distance and its capacity, plus issued I/O requests - total
count, average request size, number of I/O waits, and the average number of concurrent requests.
EXPLAIN (ANALYZE, IO, BUFFERS) SELECT count(*) FROM events WHERE ts > now() - interval '7 days';
Read it with BUFFERS: read blocks tell you what the scan asked for, the IO block tells you
how far the prefetcher ran ahead of the consumption point and how often the scan had to wait.
auto_explain gains a matching auto_explain.log_io option that adds the same reporting to
logged plans, and auto_explain.log_extension_options lets modules such as pg_plan_advice attach
their own EXPLAIN options to auto_explain output.
What changed, and in which release
| Behaviour | PG 18 | PG 19 |
|---|---|---|
| I/O worker count | Fixed at io_workers (default 3) |
Autoscaled between io_min_workers (2) and io_max_workers (8) |
| Read-ahead scheduling | Basic prefetch ordering | Improved scheduling for large requests (no new knob) |
EXPLAIN I/O visibility |
Buffer counts only | IO option: queue depth, request counts and sizes, waits, concurrency |
auto_explain |
No I/O reporting | auto_explain.log_io |
COPY FROM parsing |
Byte-wise text/CSV parsing | SIMD CPU instructions |
COPY FROM parsing belongs on this page rather than the SQL one because the speedup is in the
input path: text and CSV input now use SIMD CPU instructions, which matters most on the bulk loads
the I/O subsystem also serves.
Where it breaks
- Assuming
io_workersstill exists - the GUC is replaced by the min/max pair; a tunedpostgresql.confline for it is an unknown-setting error on 19, not a silent miss. - Judging the pool by process count - idle workers retire after
io_worker_idle_timeout, so a quietpslisting is expected, not a sign the subsystem is off. - Reading
IOwithoutANALYZE- the option reports issued requests, which exist only when the plan actually runs. - Beta caveat - GUC names and defaults are from the beta documentation.
What this is not for
- Diagnosing non-I/O waits -
IOreports prefetch and request behaviour; lock waits, client waits and CPU time needpg_stat_activityandpg_stat_io, covered in monitoring. - Replacing
pg_stat_iocapacity planning - the per-backendEXPLAINview is a microscope for one query; the cumulative view is the long-term picture.
PostgreSQL release support
5 supported| Release | Latest | Released | End of life | Status |
|---|---|---|---|---|
18 | 18.6 | Sept 2025 | Nov 2030 | Supported |
17 | 17.11 | Sept 2024 | Nov 2029 | Supported |
16 | 16.15 | Sept 2023 | Nov 2028 | Supported |
15 | 15.19 | Oct 2022 | Nov 2027 | Supported |
14 | 14.24 | Sept 2021 | Nov 2026 | Supported |
13 | 13.23 | Sept 2020 | Nov 2025 | End of life |
12 | 12.22 | Oct 2019 | Nov 2024 | End of life |
11 | 11.22 | Oct 2018 | Nov 2023 | End of life |
Data as of 2026-10-01 from endoflife.date. Refresh with node scripts/fetch-versions.mjs.
What to watch
Specific things that would change what this page concludes.
- Whether io_method gains a native io_uring method on Linux before or after GA - the release notes document the autoscaling, not a new method
Where PostgreSQL sits
How this component relates to the rest of the stack. Items without a link are not covered here yet.