Module A-8·27 min read

The most architecturally significant change to Postgres in a decade — the definitive technical breakdown.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module 8 — PostgreSQL 18: Asynchronous I/O and What It Changes

What this module covers: PostgreSQL 18 ships the most architecturally significant change to the storage layer in the project's history: native asynchronous I/O. Every module in this course has described a world where Postgres processes one I/O request at a time per process. PostgreSQL 18 changes that. This module explains what async I/O means, how it is implemented, what it changes for sequential scans, checkpoints, vacuum, and replication, and how to tune it for your workload.


The Problem: Synchronous I/O as a Throughput Ceiling

Every I/O operation in PostgreSQL prior to version 18 follows the same pattern:

  1. Backend or background process needs a page from disk
  2. It calls read() (or pread()) — a synchronous system call
  3. The OS schedules the I/O, the process blocks waiting for it to complete
  4. The page arrives, the process continues

This is the simplest model, and it works well when:

  • Data is in the OS page cache (no real I/O, cache hit is fast)
  • shared_buffers is large enough to hold the working set
  • I/O latency is not the bottleneck

The problem: on modern NVMe storage, the disk can serve multiple I/O requests simultaneously. An NVMe SSD with a 32-deep I/O queue can be saturating its bandwidth while Postgres submits exactly one request at a time per process, waits for it, then submits the next one. Postgres is artificially serializing I/O that the hardware can parallelize.

The Sequential Scan Example

A sequential scan of a 10GB table on NVMe:

Before PG18 (synchronous):

  • Process reads page 0, waits ~50μs
  • Process reads page 1, waits ~50μs
  • ...
  • Total: 1,310,720 pages × 50μs = 65 seconds

Theoretical with async (queue depth 32):

  • Process submits 32 read requests simultaneously
  • Hardware handles them in parallel
  • Effective throughput: 32 × baseline ÷ roughly same latency
  • Total: ~2 seconds at full NVMe bandwidth

The real-world gain is not 32x — there is overhead, coordination cost, and the OS page cache intervenes — but 2–5x throughput improvement on I/O-bound workloads is consistently demonstrated in benchmarks.

Why Postgres Didn't Have Async I/O Earlier

The multi-process architecture (Module 0) made async I/O difficult. In a multi-threaded database (Oracle, SQL Server), threads can submit async I/O requests and the thread pool can process completions cooperatively. In Postgres, each backend is an independent OS process. Coordinating async I/O across processes — especially for shared buffer management — required significant architectural work.

The solution that shipped in PG18 took years of design and implementation across multiple development cycles.


The PG18 Async I/O Architecture

Two Backends: io_uring and Worker Threads

PG18 implements async I/O through a pluggable backend abstraction. Two backends ship:

io_uring (Linux 5.1+):

  • Uses the Linux io_uring interface — a kernel-level async I/O ring buffer
  • Zero-copy submission: Postgres writes I/O requests to a shared ring buffer without a system call
  • Completions are polled from the completion ring
  • Lowest overhead, highest throughput on modern Linux
  • Requires Linux kernel ≥ 5.1 (practically: ≥ 5.10 LTS for production use)

Worker threads (cross-platform fallback):

  • Spawns a pool of I/O worker threads
  • Threads perform synchronous pread()/pwrite() calls on behalf of the requesting process
  • The requesting process submits work to the thread pool and continues, polling for completions
  • Works on Linux, macOS, Windows — anywhere Postgres runs
  • Higher overhead than io_uring but significantly better than per-process blocking I/O
ini

The I/O Concurrency Model

Within a single Postgres process (backend or background worker), the async I/O system allows multiple I/O requests to be in-flight simultaneously. The concurrency limit:

ini

When a process needs a page:

  1. It submits an async read request (non-blocking)
  2. It continues processing — prefetching more page requests, executing other plan nodes
  3. It polls for completion when it actually needs the page data
  4. If the page is ready, it proceeds immediately; if not, it waits briefly

This prefetch-ahead pattern is where the throughput gain comes from.


What Changes for Each Subsystem

Sequential Scans

Sequential scans see the most dramatic improvement. The executor now submits read-ahead I/O requests for upcoming pages while processing current pages.

The effective_io_concurrency parameter takes on new meaning:

ini

In practice: a sequential scan with effective_io_concurrency = 64 submits 64 page reads ahead of the current position. By the time the executor reaches those pages, they are already in the buffer cache. On cold storage, this transforms sequential scan throughput from single-queue to deep-queue performance.

sql

Checkpoints

Checkpoints (Module 3) write all dirty shared_buffers pages to disk. Before PG18, the checkpointer submitted one write at a time, with checkpoint_completion_target spreading the writes over time.

In PG18, the checkpointer submits batches of async writes. This changes the checkpoint I/O profile:

Before: flat, throttled write rate over the checkpoint window After PG18: burst submission followed by hardware-parallel completion — can complete the same work in less wall-clock time with the same I/O pressure

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.