PostgreSQL 18 AIO Tuning Guide: io_uring vs Worker vs Sync

Updated on Sep 19, 2026
Mila H
8 MINS READ
Table of Contents
PostgreSQL 18 Async I/O

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:

Bash
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:

Bash
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:

Bash
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:

Bash
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.

Bash
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.

Bash
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:

Bash
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:

Bash
sudo nano /etc/postgresql/18/main/postgresql.conf

Find and uncomment or add these lines to the file:

Bash
io_method = synceffective_io_concurrency = 1maintenance_io_concurrency = 1

Save the file, then restart PostgreSQL, since io_method needs a full restart:

Bash
sudo systemctl restart postgresql

Confirm the setting applies correctly:

Bash
sudo -u postgres psql -d aio_test -c "SHOW io_method;"

Then, connect to the test database and time a full table scan:

Bash
sudo -u postgres psql -d aio_test
SQL
\timing onEXPLAIN (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:

SQL
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:

SQL
CREATE INDEX IF NOT EXISTS idx_abalance ON pgbench_accounts(abalance);EXPLAIN (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:

Bash
sudo nano /etc/postgresql/18/main/postgresql.conf

Change the settings to:

Bash
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:

Bash
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:

SQL
ALTER SYSTEM SET io_method = 'worker';ALTER SYSTEM SET io_workers = 3;ALTER SYSTEM SET effective_io_concurrency = 16;ALTER SYSTEM SET maintenance_io_concurrency = 16;

Then restart the service so io_method applies:

Bash
sudo systemctl restart postgresql

Once you are done, repeat the same sequential scan, VACUUM, and bitmap heap scan queries from the baseline section. 

Bash
sudo -u postgres psql -d aio_test
SQL
\timing onEXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM pgbench_accounts WHERE abalance > 0;VACUUM (VERBOSE) pgbench_accounts;EXPLAIN (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:

Bash
sudo apt install liburing2 -ypg_config --configure | grep liburing

Switch the setting and restart to apply the changes:

Bash
sudo -u postgres psql -c "ALTER SYSTEM SET io_method = 'io_uring';"sudo systemctl restart postgresql

Confirm it applied:

Bash
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:

Bash
sudo -u postgres psql -d aio_test
SQL
\timing onEXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM pgbench_accounts WHERE abalance > 0;VACUUM (VERBOSE) pgbench_accounts;EXPLAIN (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.

Bash
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;"

Reading pg_stat_io

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:

Bash
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:

Bash
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.

Not much. AIO in PostgreSQL 18 only speeds up reads, including sequential scans, bitmap heap scans, and VACUUM. Writes, including WAL, are still synchronous.

The default is worker. It uses background worker processes and works on every platform, including Windows and macOS.

Not always. io_uring wins on Linux with NVMe storage, but you should benchmark both, since results depend on your kernel, storage, and workload.

Yes. Changing io_method needs a full server restart. Other settings like effective_io_concurrency only need a config reload.

Run pg_config --configure | grep liburing to see if your PostgreSQL build was compiled with liburing support, and make sure the liburing2 package is installed.