“Everyone wants the spotlight, but only few can stand the heat.”

I stopped auto-adding Redis in 2026 because PostgreSQL 18 moved the goalposts

For most of my career I treated a separate Redis box like salt to taste. Then PostgreSQL 18 shipped. This is my updated default.

TechnologyMarch 20, 2026
By Jimmy Nguyen
8 min read
🔴

A confession about my Redis reflex

For most of my career, I treated a separate Redis box like an adult version of adding "salt to taste."

📦

Need caching?

Add Redis.

📬

Need a queue?

Add Redis.

Need "speed"?

Add Redis.

It worked. It also quietly turned into a reflex, not a decision.

Then the PostgreSQL Global Development Group shipped PostgreSQL 18 (released September 25, 2025). It didn't just add another nice-to-have feature — it changed the trade-off curve for a bunch of “normal” apps. This post is my updated default: start with Postgres 18, and only add Redis when I can prove it's pulling its weight.

The 2019 rule that made sense

The old mantra — "Postgres for persistence, Redis for speed" — came from a real place.

Redis's edge

  • Sub-microsecond processing time — pure in-memory system
  • Ultra-fast key/value access at massive request rates
  • Native data structures: sets, sorted sets, streams, TTL
  • Pub/Sub with clear delivery semantics
🐘

Postgres's reality

  • Historically relied on synchronous read model for storage
  • Shared buffer cache + OS page cache hide a lot of disk latency already
  • Many “reads” are already memory hits — just not where you assumed
💡

Redis became the default not just for disk avoidance — but for its data structures, TTL semantics, and Pub/Sub that are awkward to recreate elsewhere. PostgreSQL 18 didn't “turn Postgres into Redis.” What it did is reduce the cases where you need another system just to avoid Postgres storage stalls.

What PostgreSQL 18 actually changed: async reads you can control

PostgreSQL 18 introduced an asynchronous I/O (AIO) subsystem. The big practical shift: Postgres can now queue multiple reads without blocking on each one, and overlap I/O with useful work.

Operations improved include sequential scans, bitmap heap scans, and vacuum. Benchmarking has shown up to 3× improvement in some storage-read scenarios.

PG18 surfaces this as a real config knob via io_method:

io_method = workerdefault

Async reads via worker processes. Works everywhere.

io_method = io_uringLinux only

Requires a build with liburing. Not available on managed platforms — check before assuming.

io_method = synclegacy

Keep the older synchronous behaviour. Useful for comparison or compatibility.

PostgreSQL 18 gives you a new lever. It doesn't delete physics.

The part people miss: async I/O is not a magic wand for point lookups

A lot of “Redis replaced” arguments quietly assume your bottleneck is “Postgres waiting on disk” — in the same way, for the same queries, all the time.

Where PG18 AIO helps most:

Operations that read many pages and benefit from queued/batched reads — sequential scans, bitmap heap scans, vacuum. That's literally how it's framed in the release announcement.

Where it won't move the needle:

If your slow endpoint is dominated by point lookups, index lookups, lock contention, CPU, or network — AIO may not help much.

PlanetScale's benchmark notes plainly: “Index scans don't (yet) use AIO.” That's a big deal, because many Redis-backed cache hits in typical apps are essentially point lookups.

Also, io_uring is not guaranteed to win. Benchmarks show scenarios where worker is competitive or better, and io_uring only wins in specific configurations. When someone says “Postgres 18 makes everything 50× faster”, my reaction is: which query plan, which storage, which cache state, which platform?

UUIDv7 and skip scans: less index drama, better locality

Two more PG18 features matter here: shape your data and indexes so your real workload is more likely to be fast — without building a second system to paper over bad locality.

🆔

UUIDv7 is now built-in

PG18 added uuidv7() as a native function — time-ordered, using a Unix timestamp with millisecond precision.

Random UUIDv4 primary keys scatter inserts across the B-tree, increasing page splits and reducing locality. UUIDv7 keeps inserts sequential.

If you want globally safe IDs without central coordination, Postgres 18 finally makes the ordered UUID path straightforward and supported out of the box.

🪜

Skip scan loosens the left-prefix handcuffs

PG18 introduced B-tree index skip scans — Postgres can now use a multicolumn index even when your query doesn't constrain the left-most column(s).

Real-world example: a composite index query going from ~66 ms → ~0.6 ms after skip scan, by turning “scan through everything” into multiple targeted index searches.

This reduces the “I need 4 variations of composite indexes just to cover 4 query shapes” problem — and extra indexes have real write and maintenance costs.

When Redis is still the right call

I'm not here to dunk on Redis. Redis is still incredible at what it's designed for. I'll still reach for it in these cases:

🚀

Extreme throughput

Very high-rate, very simple key/value operations with sub-microsecond processing time. Cluster-scale managed services at extreme RPS.

📡

Pub/Sub semantics

Fast, simple, at-most-once delivery. Postgres LISTEN/NOTIFY is not a drop-in equivalent — Redis is explicit about its Pub/Sub semantics.

🧱

Redis-native features

TTL-heavy workloads, cache eviction policies, sorted sets, streams — Postgres is not trying to be Redis for these.

🎯

The real point: Redis should be an answer to a measured requirement, not a default line item in docker-compose.yml.

My default architecture now

Here's what I do now when building a “normal” product service — say, a document-processing workflow (maps naturally to AI extraction pipelines, onboarding flows, back-office operations).

The goal

  • A queue for background jobs
  • A place to store job state and retries
  • A way to wake workers without hammering the DB
  • One fewer system to debug at 3 a.m.

Queue table + SKIP LOCKED worker pattern

Postgres supports SELECT ... FOR UPDATE SKIP LOCKED, which lets multiple workers safely claim jobs without stepping on each other:

-- Claim one job without fighting other workers
WITH next_job AS (
  SELECT id
  FROM jobs
  WHERE status = 'queued'
    AND run_after <= now()
  ORDER BY run_after, id
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
UPDATE jobs
SET status    = 'running',
    started_at = now()
WHERE id IN (SELECT id FROM next_job)
RETURNING *;

Wake workers without polling hell

Postgres has LISTEN / NOTIFY for async notifications. Instead of polling every 200 ms:

1

App inserts a job row into the queue table

2

App sends a NOTIFY jobs event

3

Workers are LISTEN jobs and wake up instantly

This isn't the same feature set as Redis Pub/Sub — different trade-offs, different delivery semantics. But for internal worker orchestration? Good enough, and one fewer system.

🎯

My personal “add Redis” bar in 2026

PostgreSQL 18's AIO subsystem makes me more confident the Postgres-only baseline can handle more of the read-heavy and maintenance-heavy “real world” before I pay the operational and cognitive tax of a second system.

I add Redis when I can write down at least one of these:

  • We need sub-millisecond end-to-end cache reads at very high request rates, and Postgres point lookups aren't meeting the p99 target.
  • We need Redis data structures or TTL eviction semantics as a core behaviour — not a side feature.
  • We’re building realtime fan-out where Redis Pub/Sub is a clean fit for our delivery semantics.

Old default

Reach for Redis because the problem involves any notion of “speed” or caching.

2026 default

Start with Postgres 18. Invest the energy in schema and index design first — UUIDv7 for ordered IDs, skip scans for fewer index variants.

Redis is powerful. It should be a deliberate choice, not muscle memory.

Share this article

Ask Jimmy's AI Assistant