PostgreSQL 18 shipped a new way to talk to your disk, which is a real asynchronous I/O (AIO) subsystem. Before this, PostgreSQL asked for one piece of data, waited for it, then asked for the next. PostgreSQL 18 lets the waiter take many orders at once and pick them up as they finish. On fast NVMe drives, this can make a real difference.
This guide provides PostgreSQL 18 AIO tuning from zero to a working benchmark.
What Is AIO in PostgreSQL 18
Asynchronous I/O means PostgreSQL can start several read requests and keep working while it waits for the disk to answer, instead of doing one read, waiting, then starting the next.
AIO in PostgreSQL 18 currently applies to read operations only. That means:
- Sequential scans.
SELECT on a full table.
- Bitmap heap scans. Index-then-heap lookups.
VACUUM. Reading pages to check for dead rows.
Write operations, including WAL writes, still work as before. So PostgreSQL 18 AIO tuning matters for read-heavy analytical queries and large maintenance tasks, not for write-heavy transaction workloads.
The io_method Setting in PostgreSQL 18
PostgreSQL 18 adds one key setting, named io_method, with three values:
sync: The old way, one request at a time. Kept for compatibility.
worker: Helper processes handle I/O at the same time; this is the default and works everywhere.
io_uring: Uses a fast Linux kernel feature for the lowest overhead, but needs Linux with liburing installed.
Changing io_method needs a full restart, not just a reload.
Installing PostgreSQL 18 on Ubuntu
We assume you have Ubuntu 22.04 or newer. We'll use the official PostgreSQL PGDG repo to install PostgreSQL 18. This gives you the real latest version, like 18.6, instead of an old one stuck in your distro's default repo.
Step 1: Add and Install PostgreSQL Repository
First, you must use the commands below to add the repository:
sudo apt updatesudo apt install postgresql-common ca-certificates -ysudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
This script automatically detects your Ubuntu version and sets up the correct repository file for you.
Then, run the system update and install PostgreSQL 18:
sudo apt updatesudo apt install postgresql-18 postgresql-contrib-18 -y
postgresql-contrib-18 gives you extra tools we'll use later for benchmarking.
Step 2: Check the Version
To verify your installation, use the commands below to check the version:
psql --versionsudo -u postgres psql -c "SELECT version();"
You should see PostgreSQL 18.6 or newer.
Step 3: Check liburing Support for io_uring
Now run the command below to check the liburing support:
sudo -u postgres psql -c "SHOW io_method;"pg_config --configure | grep -o liburing
If your build does not list liburing, you can still test sync and worker, but io_uring will not be available. Most PGDG packages on Linux ship with liburing support already compiled in.
Setting Up a Test Database
At this point, you can create a dedicated test database and load it with enough data that it doesn't all fit in memory. This is important because AIO effects show up when PostgreSQL actually has to hit the disk.
sudo -u postgres createdb aio_testsudo -u postgres psql -d aio_test -c "CREATE EXTENSION IF NOT EXISTS pgbench;"
Load a Large Test Dataset with pgbench
We'll use pgbench to generate a dataset bigger than RAM, so reads actually go to disk instead of being served from the buffer cache.
sudo -u postgres pgbench -i -s 400 aio_test
The -s 400 creates about 6 GB of data, around 40 million rows. If your server has more than 8 GB of RAM, use a higher scale factor. This makes sure the data is too big to fully fit in cache, so your PostgreSQL 18 AIO tuning tests hit the real disk, not memory.
Clear the Cache Before Every Test Run
Clear the cache before every test round so your numbers are accurate and repeatable:
sudo systemctl stop postgresqlsudo syncecho 3 | sudo tee /proc/sys/vm/drop_cachessudo systemctl start postgresql
Do this before every test below. If you skip it, your results will look confusing or inconsistent.
Setting Your Baseline with io_method = sync
Let's start with the old method, sync. This gives us a starting number so we can see how much worker and io_uring actually improve things later.
Edit postgresql.conf with your desired text editor like nano:
sudo nano /etc/postgresql/18/main/postgresql.conf
Find and uncomment or add these lines to the file:
io_method = synceffective_io_concurrency = 1maintenance_io_concurrency = 1
Save the file, then restart PostgreSQL, since io_method needs a full restart:
sudo systemctl restart postgresql
Confirm the setting applies correctly:
sudo -u postgres psql -d aio_test -c "SHOW io_method;"
Then, connect to the test database and time a full table scan:
sudo -u postgres psql -d aio_test
1\timing on2EXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM pgbench_accounts WHERE abalance > 0;
Write down the Execution Time and the number of physical reads shown under Buffers: shared read=. This is your sync baseline for PostgreSQL 18 AIO tuning comparisons.
Now use the command below to run a VACUUM benchmark:
VACUUM (VERBOSE) pgbench_accounts;
Note the time and the pages scanned. VACUUM is one of the three operations that benefit directly from AIO.
Finally, use the commands below to run a bitmap heap scan benchmark:
1CREATE INDEX IF NOT EXISTS idx_abalance ON pgbench_accounts(abalance);2EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts WHERE abalance BETWEEN -100 AND 100;
Check the query plan output for Bitmap Heap Scan; that confirms you're testing the right operation.
Testing io_method = worker
Now switch to the worker method, which is the default in PostgreSQL 18. Background worker processes handle I/O requests so PostgreSQL doesn't block on a single read at a time. Edit the config file again:
sudo nano /etc/postgresql/18/main/postgresql.conf
io_method = workerio_workers = 3effective_io_concurrency = 16maintenance_io_concurrency = 16
io_workers = 3 is the PostgreSQL default and a safe starting point. A simple starting point is io_workers set to about 25% of your CPU cores. Only go higher if testing shows it actually helps.
Restart and drop caches again before testing:
sudo systemctl restart postgresqlsudo syncecho 3 | sudo tee /proc/sys/vm/drop_cachessudo systemctl start postgresql
Alternatively, you can change most of these live with SQL and just restart for io_method:
1ALTER SYSTEM SET io_method = 'worker';2ALTER SYSTEM SET io_workers = 3;3ALTER SYSTEM SET effective_io_concurrency = 16;4ALTER SYSTEM SET maintenance_io_concurrency = 16;
Then restart the service so io_method applies:
sudo systemctl restart postgresql
Once you are done, repeat the same sequential scan, VACUUM, and bitmap heap scan queries from the baseline section.
sudo -u postgres psql -d aio_test
1\timing on2EXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM pgbench_accounts WHERE abalance > 0;3VACUUM (VERBOSE) pgbench_accounts;4EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts WHERE abalance BETWEEN 100 AND 100;
Compare the times with your sync results. Most people see the biggest gains with sequential scans and VACUUM, since both read large amounts of data sequentially.
Testing io_method = io_uring
io_uring uses a modern Linux kernel feature that lets PostgreSQL submit and collect I/O requests with less overhead than the worker method. This is where PostgreSQL 18 AIO tuning shows its best numbers on NVMe hardware, but it only works on Linux with liburing installed. Check liburing is installed:
sudo apt install liburing2 -ypg_config --configure | grep liburing
Switch the setting and restart to apply the changes:
sudo -u postgres psql -c "ALTER SYSTEM SET io_method = 'io_uring';"sudo systemctl restart postgresql
sudo -u postgres psql -c "SHOW io_method;"
If it shows worker instead of io_uring, your build lacks liburing support. In that case, stick with worker, which works everywhere and is much faster than sync.
Again, repeat the benchmarks:
sudo -u postgres psql -d aio_test
1\timing on2EXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM pgbench_accounts WHERE abalance > 0;3VACUUM (VERBOSE) pgbench_accounts;4EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pgbench_accounts WHERE abalance BETWEEN 100 AND 100;
Now you have three sets of results from sync, worker, and io_uring. Comparing them side by side on real NVMe hardware is what tells you if PostgreSQL 18 AIO tuning actually helps your workload.
How to Read pg_stat_io Output
Don't just trust the timer. PostgreSQL 18 added new columns to pg_stat_io that show the real bytes read, written, and extended, replacing the old op_bytes column.
sudo -u postgres psql -d aio_test -c "SELECT backend_type, object, context, reads, read_bytes, writes, write_bytes FROM pg_stat_io WHERE reads > 0 ORDER BY read_bytes DESC;"

Run this after each test round, before you clear the cache for the next one. It shows exactly how many bytes each I/O method read from disk. PostgreSQL 18 also tracks WAL I/O here now, under the wal object type.
To reset the counters between test rounds, run:
sudo -u postgres psql -c "SELECT pg_stat_reset_shared('io');"
Simulating Real Traffic with pgbench
One query doesn't tell the full story. This test uses pgbench to simulate many users reading data at the same time, closer to real traffic:
sudo -u postgres pgbench -c 10 -j 4 -T 60 -S aio_test
This runs 10 clients and 4 threads for 60 seconds, using the -S flag for read-only queries. Run it once for each io_method, clearing the cache between runs, and compare the TPS and latency numbers.
Conclusion
PostgreSQL 18 AIO tuning is a set of small tests you run on your own hardware. Start with a sync baseline, move to worker, then try io_uring if you're on Linux with liburing. On slow disks or cloud storage, the gains may be small. On real NVMe hardware, the difference between sync and io_uring can be big for read-heavy and maintenance work.
Want to run these same benchmarks on hardware built for this? Try a PerLod NVMe dedicated server.
Or start smaller with our Linux VPS for PostgreSQL guide.
We hope you enjoy this guide. For the official technical background on this feature, see the PostgreSQL 18 Press Kit.