PostgreSQL 18 is the first release in years where the storage engine, not the query planner, is the reason to upgrade. Its new asynchronous I/O subsystem lets backends issue many disk reads at once instead of stalling on each one, and the official benchmarks show cold reads running up to 3x faster. But that headline number comes with a shape most summaries skip: the gain is large on cold sequential and bitmap scans, near zero on warm-cache and write-heavy workloads, and exactly zero if your node image can't do io_uring. On owned hardware, PostgreSQL 18 is a faster database behind a short checklist — and the upgrade itself is still yours to run.
Here is the verdict up front, with the sensitivity the single headline number hides:
| Workload shape | io_method=sync (PG17 behavior) | worker | io_uring |
|---|---|---|---|
| Cold sequential scan, data on NVMe | Baseline | ~1.6x faster | ~2.8x faster |
| Cold bitmap heap scan | Baseline | Faster | Fastest (biggest win in published tests) |
| Warm cache (shared_buffers hit) | Baseline | ~0 gain | ~0 gain |
| OLTP write-heavy (WAL-bound) | Baseline | ~0 gain | ~0 gain |
The cold-scan figures come from published PostgreSQL 18 benchmark decks: a PGDU conference deck measured sync as baseline, worker at 1.6x, and io_uring at 2.8x on the same read workload, while pganalyze's webinar showed a 100M-row COUNT(*) dropping from 10.1s (worker) to 5.7s (io_uring). The zeros are just as important: AIO accelerates reads issued through the new read-stream interface, so workloads that rarely touch disk, or that wait on WAL writes instead, see nothing. Know which workload you run before you promise anyone a speedup.
What PostgreSQL 18 ships that an operator actually cares about
Beyond AIO, version 18 (September 2025) packs several operator-relevant changes. Each row below is the feature plus the reason it matters to whoever owns the machines:
| Feature | What it is | Why the operator cares |
|---|---|---|
Async I/O (io_method) | sync, worker (portable), or io_uring (Linux) read paths | The upgrade's headline payoff; see the checklist below before expecting it |
uuidv7() | Native time-ordered UUID generation | Better B-tree locality for new primary keys; see the rule below |
| B-tree skip scan | Multicolumn indexes serve queries that omit the leading column | Fewer redundant indexes to maintain on wide tables |
| Virtual generated columns by default | Computed at read time unless declared STORED | Gotcha: virtual columns can't be indexed — audit schemas that assumed STORED |
| Checksums on by default | New clusters get data checksums without a flag | Silent-corruption detection for free on fresh installs |
pg_upgrade keeps planner stats | Statistics survive major upgrades | No post-upgrade ANALYZE storm before plans stabilize |
| OAuth authentication | Token-based auth alongside SCRAM/certs | One less password to rotate for app fleets |
One scope note that saves confusion later: PostgreSQL 18's AIO covers reads only — sequential scans and bitmap heap scans issued through read streams. The write path, WAL, and checkpoints are unchanged; asynchronous writes and production direct I/O are future-direction work, not part of this release. If your bottleneck is write throughput, 18's headline feature is not your fix.
The uuidv7 rule: new keys yes, rewrites no
uuidv7() deserves its own paragraph because the title promised it and the decision is genuinely binary. Version 7 UUIDs embed a timestamp in the high bits, so monotonically-inserted keys land in adjacent B-tree pages instead of scattering across the index the way random v4 values do. Less scatter means less page splitting and less index bloat over time.
The operator rule is simple: use v7 for new primary keys, leave existing v4 keys alone. Rewriting a live primary key to change its UUID version buys locality at the cost of a full table rewrite plus every foreign key that references it — a migration whose risk dwarfs the fragmentation it cures. Adopt v7 at the next schema you design, not as a retrofit project.
The owned-NVMe checklist before io_uring pays
On a managed service, someone else answers every question below. On machines you own — a Hetzner box, a Cluster API-provisioned node, a Kubernetes StatefulSet with local NVMe — each one is yours. Walk the list in order; the first "no" is where your speedup dies.
| # | Check | How to verify |
|---|---|---|
| 1 | Kernel ≥ 5.1 with io_uring enabled | uname -r; confirm the node image doesn't disable io_uring via sysctl or block it in seccomp |
| 2 | Postgres built with liburing | SHOW io_method accepts io_uring, or check the build's configure summary; a build without liburing silently can't select it |
| 3 | Container seccomp permits io_uring syscalls | Container runtimes with default seccomp profiles can block io_uring_enter; test the actual pod spec, not the host |
| 4 | Filesystem and direct-I/O alignment sane | NVMe volume formatted and mounted per your distro's guidance; misaligned direct I/O turns fast reads into read-modify-write penalties |
| 5 | io_workers, io_combine_limit, io_max_concurrency tuned | Start from defaults, then measure; combining adjacent reads into fewer, larger I/Os is where much of the win lives |
| 6 | Verify with real plans, not vibes | EXPLAIN ANALYZE now shows BUFFERS by default in 18; cross-check AIO behavior with the new pg_aios view |
Two notes on the table. First, check 3 bites Kubernetes operators most often: the host kernel can be perfectly capable while the container's seccomp profile rejects the exact syscalls io_uring needs. The failure mode is a fallback or an error at startup, not a helpful warning mid-query.
Second, check 5's tunables interact with your storage latency. High-latency network-attached volumes benefit from deeper concurrency than local NVMe, where published tests show the gap between worker and io_uring narrowing as latency drops. Measure on your own volume type.
The upgrade path, button vs runbook
Also in September, Railway shipped the thing every self-hosted operator is now implicitly compared against: one-click Postgres major version upgrades. From Database → Config → Major Version Upgrade, you review a compatibility report, pick a target (14 through 17 to any newer major Railway publishes), and Railway takes a backup, upgrades in place, redeploys, takes another backup, and tracks progress in one flow. HA clusters get the primary upgraded and replicas rebuilt. The database is unavailable for typically a few minutes depending on data size, and a limited rollback window restores the pre-upgrade backup — discarding writes made after the upgrade.
The CloudNativePG equivalent is genuinely declarative, but "declarative" and "one click" are not the same job. Since CNPG 1.26, bumping a cluster's imageName to a higher PostgreSQL major triggers an offline in-place upgrade: the operator shuts the cluster down safely, runs pg_upgrade, and brings it back on the new major (PostgreSQL 18 images are supported from CNPG 1.29). Side by side:
| Step | Railway one-click | CNPG declarative upgrade |
|---|---|---|
| Trigger | Dashboard button + target picker | Edit imageName in the Cluster manifest |
| Pre-flight | Generated compatibility report | You, reading the PG 18 release notes and extension changelogs |
| Backup | Automatic before and after | Your backup schedule (Velero/CNPG scheduled backups) — verify restorability first |
| Downtime | Minutes, tracked in the dashboard | Whole cluster offline during pg_upgrade; duration scales with catalog size |
| HA handling | Primary upgraded, replicas rebuilt | Replicas shut down with the cluster and rejoin after |
| Image constraint | Eligible versioned official images | New image must share the old image's OS distro (no bullseye → bookworm jump mid-upgrade) |
| Rollback | Limited window, restores pre-upgrade backup, loses post-upgrade writes | Your rehearsed restore procedure — same data-loss shape, but you own the runbook |
What the button hides, enumerated honestly, is the interesting part: compatibility checking, backup verification, extension gating, and the rollback rehearsal. PostGIS, pgvector, and friends must each have a PostgreSQL 18 build or the upgrade fails closed.
CNPG automates the pg_upgrade mechanics — the shutdown ordering, the binary invocation, the catalog migration — which is real labor saved over hand-rolled upgrades. It does not automate the judgment: which extensions are ready, whether your backup actually restores, or how long your catalog takes. Budget the rehearsal, not just the manifest edit.
The Monday-morning runbook
Concretely, for the operator who read this far and wants the speedup:
- Clone and rehearse. Restore a production backup to a scratch cluster and run the full CNPG image-bump upgrade there first. Time it; that duration is your maintenance window, not a guess.
- Gate extensions. List every extension in
pg_extension, confirm each ships a PostgreSQL 18 build for your CNPG image variant, and pin versions. One unready extension blocks the whole upgrade. - Pick
io_methodper node capability.io_uringwhere checks 1–3 pass,workereverywhere else. Both beatsyncon cold reads; neither helps warm or write-bound workloads. - Measure before and after. Capture
pgbenchread profiles andEXPLAIN (ANALYZE, BUFFERS)output for your five slowest read queries on the old major, then re-run identical workloads after. If the numbers don't move, re-check the checklist before blaming the database. - Keep the rollback path hot. Know exactly which backup restores to which point, and how long the restore takes. Railway's rollback window discards post-upgrade writes; yours will too — the difference is whether you told your team that before the upgrade or after.
Own the machines, own the upgrade
PostgreSQL 18 rewards operators and punishes assumptions: the AIO win is real but workload-shaped, io_uring is fast but gated on kernel, build, and seccomp facts nobody checks until something is slow, and the CNPG path to get there is declarative without being effortless. The good news is that the observability gap is closing — PostgreSQL 19 development is already adding EXPLAIN ANALYZE visibility into AIO behavior through an IO option, so the next upgrade's "did it actually use async reads" question gets a plan-node answer instead of inference from timings.
The managed button will keep getting better; Railway's September release is proof of the direction. But every item in the checklist and runbook above is also knowledge that transfers — to the next major, to the next storage backend, to the 3 AM page the button can't answer. That is the actual trade of operator-owned data: nobody does it for you, and nobody can take the understanding away.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



