PostgreSQL 18 shipped three features that sound like an easy "turn them all on" checklist: asynchronous disk I/O, native OAuth 2.0 authentication, and a faster pg_upgrade --swap mode. Only one of the three is safe to flip on by default today. The other two have a build flag, a kernel requirement, or an operator gap standing between "released" and "usable on your fleet" — and finding out which is which by reading the release notes alone takes longer than it should.
The Recommendation, Up Front
Here's the verdict for a Postgres image managed by CloudNativePG (CNPG) or the Zalando postgres-operator — the two HA-Postgres operators this blog already recommends for a self-hosted PaaS's control-plane database:
| Feature | Turn on now? | Why |
|---|---|---|
Async I/O (io_method) | Yes, worker (the default); try io_uring on NVMe nodes | Reads only, 2-4x on sequential/bitmap-heap scans and vacuum; io_uring needs a --with-liburing build and Linux |
| OAuth 2.0 authentication | Not yet, for most fleets | Needs a --with-libcurl build most distro packages skip, plus an external IdP already in your stack |
pg_upgrade --swap | Manual only, not via CNPG/Zalando | Both operators' declarative upgrade path still hard-codes --link; --swap isn't wired in yet |
The rest of this post is the reasoning behind each row — what each feature actually buys, what it costs to enable, and what changes once operator support catches up.
Async I/O: The One With No Catch, As Long As You Read the Fine Print
PostgreSQL 18 replaces its synchronous, one-request-at-a-time storage read path with a real asynchronous I/O subsystem, controlled by a new io_method parameter with three settings: sync (the old behavior), worker (a pool of background processes issuing reads on the backend's behalf — the new default), and io_uring (Linux's native async I/O interface, batching read requests through a shared ring buffer with the kernel).
The performance case is real. Benchmarks from pganalyze, PlanetScale, and credativ put sequential scans, bitmap heap scans, and VACUUM at 2-4x faster on NVMe and cloud block storage once io_uring is in the mix — the exact query shapes a multi-tenant control-plane database runs constantly during reconciliation loops and backup verification.
Three caveats keep this from being an unconditional win:
- It's read-only. Writes and WAL flushes still go through the old synchronous path. A write-heavy tenant workload sees none of this speedup.
- Index scans aren't wired up yet. AIO currently accelerates sequential and bitmap heap scans, not plain index scans — so the gain is workload-shape-dependent, not a flat multiplier across every query.
io_uringisn't free to enable. It requires PostgreSQL to be compiled--with-liburing, a Linux kernel new enough to support it well, and (per multiple postmortems from early adopters) a kernel/filesystem combination that's actually been tested — some older kernel/io_uringcombinations have had correctness bugs unrelated to Postgres itself.
The safe default is worker, which is what PostgreSQL 18 ships as its out-of-the-box setting and what CNPG and Zalando operator images inherit without any extra configuration — you get some of the async benefit with none of the build risk. io_uring is worth an explicit opt-in trial on NVMe-backed node pools specifically, not a fleet-wide flip, until you've confirmed your kernel version and workload shape actually land in the part of the benchmark curve that benefits.
OAuth: A Real Feature With a Build Flag Standing in the Way
PostgreSQL 18 turns the server into an OAuth 2.0 resource server — it can validate bearer tokens issued by an external identity provider instead of checking a password or certificate. On the client side, libpq implements the OAUTHBEARER SASL mechanism and, notably, the OAuth Device Authorization flow (RFC 8628) — the same flow you already use to log into a smart TV or a CLI tool: the client prints a URL and a short code, you approve it in a browser, and the client polls until a token shows up. That makes it usable even from a machine with no browser of its own, which is exactly the shape of a CI runner or an agent-driven deploy pipeline authenticating to a database.
For a self-hosted PaaS, that's a genuinely attractive story for control-plane credentials — replacing a rotated static password for an operator's own service accounts with short-lived, IdP-issued tokens, auditable through whatever OAuth provider you already run for the rest of the platform. Connecting looks like this once it's wired up:
psql "postgres://operator@db.internal:5432/control_plane?oauth_issuer=https://idp.internal/auth&oauth_client_id=cnpg-operator"The catch is that none of this is a flip-a-flag change. Both the server and libpq need --with-libcurl at build time to get OAuth support at all, and as of PostgreSQL 18's release, most distributions and prebuilt images don't enable it by default — which includes the stock CNPG and Zalando operator images. Enabling it means building a custom Postgres image with the flag set, standing up (or already having) an OAuth provider that can issue tokens scoped correctly for database access, and validating the whole OAUTHBEARER handshake end-to-end before trusting it for anything that gates access to tenant data.
That's a reasonable project for a platform team, not a checkbox. The honest recommendation: keep OAuth on the roadmap for control-plane service-account credentials specifically — it's a strictly better rotation story than long-lived passwords once built — but don't block a PostgreSQL 18 upgrade on shipping it, and don't expect stock operator images to grow OAuth support for free.
pg_upgrade --swap: Faster, But Your Operator Doesn't Use It Yet
The third headline feature is a new pg_upgrade mode. The existing --link mode hard-links each old data file into the new cluster's directory — fast, but it leaves the old cluster's files entangled with the new one until a vacuumdb --analyze-in-stages and a manual cleanup. --swap instead moves the entire data directory from the old cluster to the new one and drops in the freshly generated catalog files, and multiple 2026 write-ups report it out-performing --link specifically on clusters with many relations — thousands of tables and indexes, where --link's per-file linking overhead adds up.
The tradeoffs are concrete: --swap requires the old and new data directories to sit on the same filesystem, and once the swap step starts, the old cluster is gone — there's no rollback path the way a still-intact --link'd old cluster offers. It's a "you're committed" upgrade mode, appropriate once you trust the upgrade and want the fastest possible cutover on a large cluster.
Here's the gap that matters for anyone running Postgres under an operator instead of by hand: CNPG's declarative offline in-place major upgrade — the feature that lets you bump a PostgreSQL major version by editing the cluster's image field — runs pg_upgrade with --link, not --swap, as of the 1.26/1.27 releases that added PostgreSQL 18 support. The Zalando operator's upgrade path doesn't expose --swap either. Neither project has wired the new mode into its controller yet.
That means a CNPG- or Zalando-managed cluster gets PostgreSQL 18's storage engine, async I/O, and (eventually) OAuth for free through a routine image bump — but a many-relation cluster doesn't get the faster upgrade mode through the operator's declarative path. If you're running a control-plane database with genuinely large table counts and want --swap's speed, that currently means stepping outside the operator: draining the cluster, running pg_upgrade --swap by hand against the same-filesystem data directories, and reconciling the operator's view of cluster state afterward — real manual work, not something to script into a Tuesday-night rollout until CNPG or Zalando ship native support.
What This Means for a Self-Hosted PaaS's Default Image
None of bex's tenant databases are bex's problem to manage — that's a deliberate non-goal, not an oversight, and nothing here changes that. But the platform's own control-plane Postgres, running under CNPG on owned Hetzner nodes, is exactly the kind of cluster this list is written for. The concrete pin for that image today: PostgreSQL 18, io_method=worker as the safe default with io_uring as an explicit experiment on NVMe node pools, OAuth left off until a custom build and IdP integration are worth the engineering time, and any large-table-count major upgrade run by hand with --swap rather than waiting on the operator to catch up. Revisit the OAuth and --swap rows the next time CNPG or Zalando cut a release — this is a decision with a shelf life, not a permanent answer.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Bex doesn't manage tenant databases (a deliberate non-goal), but its Cluster API-provisioned fleet runs the same CNPG-backed control-plane Postgres this post is about. Star the repo on GitHub or deploy your first app today.
Sources
- Waiting for Postgres 18: Accelerating Disk Reads with Asynchronous I/O — pganalyze
- Benchmarking Postgres 17 vs 18 — PlanetScale
- PostgreSQL 18 Asynchronous Disk I/O: Deep Dive Into Implementation — credativ
- PostgreSQL Documentation: 32.20. OAuth Support (libpq)
- Preview PostgreSQL 18's OAuth2 Authentication (1) — EnterpriseDB
- PostgreSQL 18: "swap" mode for pg_upgrade — dbi services
- PostgreSQL Documentation: pg_upgrade
- Offline In-place Major Upgrades with CloudNativePG — EnterpriseDB
- CloudNativePG 1.27.1, 1.26.2 and 1.25.4 released!
- Add support for PostgreSQL 18 — zalando/postgres-operator, GitHub Issue #2958