My last article – “PostgreSQL 18 vs Redis: The Day I Realized the Cache Layer Could Be Deleted” – got some interesting reactions in the comments:
“This is just theory. Where’s the data?” “Your little test means nothing. Try it in production.” “Redis does 0.1ms and you want to replace it with PG? You’re joking.”
Honestly, I was happy to see those comments. Skepticism boils down to one thing: show me the proof.
You want data? Fine. Here’s plenty.
Quick Recap
The core argument from last time was simple: after PostgreSQL 18 introduced async IO (io_uring) and B-tree skip scan, many queries that used to need a Redis cache layer to hit acceptable speeds can now be handled by the database alone. The performance gains are large enough to make you rethink your entire caching architecture.
The data I gave last time was from our internal tests and admittedly rough. This time, I’m putting our numbers, other people’s numbers, and academic research all on the table. Let’s see how far async IO and skip scan can push database performance.
Dataset 1: Real Production, 1K Concurrent Users
This data comes from a real SaaS backend system – not one of those toy benchmarks that runs SELECT 1 and declares “performance doubled.”
Test conditions:
- 1,000 concurrent users
- 90% reads, 10% writes
- Data: 15 million orders, 2 million customers
- Query type: customer dashboard (profile + recent orders + account status)
Here’s what the query looks like:
SELECT o.id, o.total_amount, o.status, o.created_at
FROM orders o
WHERE o.customer_id = $1
AND o.status IN ('PAID', 'SHIPPED')
ORDER BY o.created_at DESC
LIMIT 20;
Nothing fancy. Fetch orders by customer, sort by date, grab the latest 20. This kind of query runs millions of times a day in any e-commerce system.
Results:
| Setup | P95 Latency | Cache Hit Rate | Notes |
|---|---|---|---|
| Redis + PostgreSQL | 72ms | 91% | Occasional cache stampede |
| PostgreSQL 18 (direct) | 54ms | – | No external cache |
You read that right. Removing Redis dropped P95 latency by 25%.
“Wait, isn’t Redis 0.1ms? How is adding Redis slower?”
Good question. This is the biggest misconception people have about caching – the 0.1ms you see is the hit case, but caches don’t hit 100% of the time.
91% hit rate sounds decent, right? But what happens with the other 9%? The request goes to Redis, finds nothing, goes to PostgreSQL for the data, then writes back to Redis. That round trip is slower than just querying the database directly.
Worse is the “occasional cache stampede.” When hot keys expire at the same time, hundreds of requests slam into the database simultaneously. Both Redis and PostgreSQL spike. This happens especially during sales events.
PostgreSQL 18 direct? Every request takes one path, gets its answer, done. No hit-or-miss gamble, no extra network hop. Simpler path, faster result.
Dataset 2: Cold vs Hot Cache Comparison
Data mentioned in the last article, now fully expanded.
Same query (user activity aggregation over 30 days), different PostgreSQL versions:
SELECT date, sum(events)
FROM activity
WHERE user_id = $1
AND date > now() - interval '30 days'
GROUP BY date;
| Scenario | PostgreSQL 17 | PostgreSQL 18 |
|---|---|---|
| Cold cache (data not in memory) | 12-18ms | 6-8ms |
| Hot cache (data in memory) | 4-6ms | 1.8-2.5ms |
PostgreSQL 18 hot cache query: around 2ms.
Now let’s calculate Redis’s real latency. Not the idealized 0.1ms – the weighted average:
- Hit (73%): 0.1ms + 0.3ms (serialization) + 0.8ms (network) = 1.2ms
- Miss (27%): 1.2ms + 15ms (DB query) + 1.5ms (write-back) = 17.7ms
- Weighted average: 0.73 x 1.2 + 0.27 x 17.7 = 5.7ms
Redis weighted average latency: 5.7ms. PostgreSQL 18 hot cache: 2ms. Which is faster?
“But our cache hit rate is 95% or even 99%!”
If you can consistently maintain 99% hit rate, then yes, Redis still has a latency advantage. But ask yourself: what does it cost to maintain 99%? Warm-up strategies, invalidation logic, dual-write consistency, TTL tuning… have you factored in that engineering cost?
Dataset 3: Third-Party Benchmarks
Don’t trust my data? Fair enough. Here’s what others found.
pganalyze (May 2025)
pganalyze is a well-respected monitoring service in the PostgreSQL ecosystem. They ran detailed async I/O benchmarks during the PostgreSQL 18 Beta period. Their conclusion: io_uring is the recommended setting for maximizing I/O performance in Postgres 18.
PlanetScale
PlanetScale (yes, the MySQL hosting company) also ran PostgreSQL 17 vs 18 comparisons. Interesting findings:
- Sequential scans and bitmap heap scans: io_uring clearly faster
- Index scans: io_uring doesn’t cover these yet, limited improvement
- Overall: read performance significantly improved through async I/O
Brute-Force Test on 100 Million Rows
Someone ran SELECT count(*) FROM test_data on 100 million rows. Simple and brutal:
| IO Method | Time |
|---|---|
| worker (default) | ~24,791ms |
| io_uring | ~7,237ms |
3.4x speed difference. Full table scan on 100 million rows, from 25 seconds down to 7.
Academic Research (PVLDB 2026)
Even academia has weighed in. A paper published in PVLDB (a top database venue) specifically studied PostgreSQL’s io_uring integration and found 14% additional performance improvement after applying optimization guidelines.
The paper also noted current limitations: PostgreSQL’s multi-process architecture restricts what io_uring can do. If PostgreSQL moves to multi-threading, performance could climb further.
Why Is Removing Redis Actually Faster? (The Real Database Performance Leap)

It’s not that Redis is slow. It’s that the entire caching architecture carries hidden costs. It’s like having a supermarket right downstairs but insisting on driving to a convenience store in the next neighborhood – the store has cheaper prices, sure, but the time you spend getting there costs more than you save.
These hidden costs are easy to overlook:
First, the network round trip. One hop to Redis and back, typical latency 0.5-1ms. If Redis is in another availability zone, maybe 2-3ms. You pay this whether the cache hits or not.
Then serialization. Stuffing a JSON object into Redis requires serialization, pulling it out requires deserialization. Complex objects: 0.3-0.5ms. Your monitoring dashboard won’t show this cost, but it’s eating CPU all the same.
There’s also the dual-path branching problem. Your code has two paths: hit goes to A, miss goes to B. Every API’s response time becomes a probability distribution instead of a deterministic value. P99 latency is all over the place, and tuning it makes you want to flip your desk.
Cache stampede is another trap. The instant a hot key expires, dozens or hundreds of requests punch through simultaneously. You need mutex locks, warm-up logic, degradation strategies. One cache layer spawns three layers of defensive code.
And finally, data consistency. The database is updated, the cache still holds the old value. Users complain “I just changed my info, why isn’t it showing?” – support tickets pile up.
PostgreSQL 18 direct connection zeros out all these costs. One data source, one path, one codebase.
The Configuration Is Dead Simple
Mentioned in the last article, posting again because it really is just a few lines:
# postgresql.conf
# Enable io_uring async IO (requires Linux kernel 5.1+)
io_method = 'io_uring'
# Concurrent read count
effective_io_concurrency = 200
maintenance_io_concurrency = 50
# Shared buffers (adjust for your memory, recommended 25% of total)
shared_buffers = 16GB
# Read-ahead merge
io_combine_limit = 512kB
Change, restart, done. No application code changes, no six-month migration project. Async IO is a “configure and enjoy” feature – change a few settings and reap the benefits.
If your Linux kernel is below 5.1 and io_uring isn’t available, you can use io_method = 'worker'. Worker mode is still faster than PostgreSQL 17’s synchronous I/O, just not as aggressive as io_uring.
Skip Scan: The Underrated Killer Feature
Everyone’s focused on io_uring, but B-tree skip scan might have a more direct impact on real-world database performance.
Here’s a concrete example. You have an orders table with a composite index on (user_id, status, created_at). Standard stuff.
Now you have a query looking for all “pending” orders without filtering by user:
SELECT * FROM orders
WHERE status = 'pending'
AND created_at > '2025-01-01'
ORDER BY created_at DESC
LIMIT 50;
In PostgreSQL 17, since the WHERE clause doesn’t include the first column of the index (user_id), the database can’t efficiently use this composite index. It either does a full table scan or takes a suboptimal path. The fix? Redis cache with a 60-second TTL.
PostgreSQL 18 skips the first index column and performs range scans for each distinct user_id. Result:
- PostgreSQL 17: 45ms
- PostgreSQL 18: 8ms
45ms to 8ms. The database engine just got smarter. No cache needed.
When You Still Need Redis
I’m not here to bash Redis. Said it last time, saying it again – these scenarios still favor Redis:
Session storage. Sub-millisecond responses with zero jitter. User login state must be instant.
Rate limiting. INCR + TTL is Redis’s bread and butter. Atomic operations, natural fit.
Pub/Sub. Real-time message distribution across services. Redis Pub/Sub is more mature than PostgreSQL’s NOTIFY.
Leaderboards. ZADD + ZRANGE – sorted sets are purpose-built for rankings.
Distributed locks. The SETNX pattern is battle-tested.
These scenarios use Redis’s data structure capabilities, not as “a read cache for PostgreSQL.”
The “read cache for PostgreSQL” use case? In many scenarios, it’s no longer necessary.
When This Doesn’t Work
Being honest here. Don’t rush to delete Redis if:
- P99 must be under 2ms: PostgreSQL 18 hot cache gets to about 2ms, but strict sub-millisecond SLAs still need an in-memory solution.
- Single hot key at millions of QPS: Every request hitting the same row – connection pools can’t handle it.
- Linux kernel below 5.1: io_uring unavailable, async I/O benefits are reduced.
- Spinning disks: Async I/O helps but it’s not magic. SSDs are a prerequisite.
- Cloud provider limitations: Some managed PostgreSQL services haven’t exposed io_uring configuration yet. Check with your provider before upgrading.
The Picture
Before upgrade:
┌───────────────┐
│ Application │
└──┬─────────┬──┘
│ │
v v
┌──────┐ ┌──────────┐
│Redis │ │PostgreSQL│
│Cache │ │ Storage │
│~0.1ms│ │ ~5-15ms │
└──┬───┘ └──────────┘
│ ^
└──────────┘
Cache invalidation
nightmare
After upgrade:
┌───────────────┐
│ Application │
└──────┬────────┘
│
v
┌────────────────┐
│ PostgreSQL 18 │
│ io_uring + AIO │
│ ~2-8ms │
└────────────────┘
One fewer component, one fewer failure mode. The odds of getting paged at 3 AM just went down.
Summary for the Skeptics
Last article relied on reasoning. This one relies on data. Here’s the summary:
| Metric | Redis + PG17 | PG18 Direct | Source |
|---|---|---|---|
| P95 latency (1K concurrent) | 72ms | 54ms | Real production |
| Hot cache query | 4-6ms | 1.8-2.5ms | Internal test |
| Sequential scan (100M rows) | – | 3.4x faster | Third-party benchmark |
| Index skip scan | 45ms | 8ms | Internal test |
| Components to maintain | 2 | 1 | Common sense |
Remember this: the real latency of a cache layer is not the hit latency – it’s the weighted average of hits and misses.
What does that mean? You can’t look at Redis’s 0.1ms hit time and call it fast. Factor in the miss penalty, serialization overhead, network round-trip cost, and the occasional cache stampede. That weighted value, in many real-world scenarios, is higher than PostgreSQL 18 direct.
What to Do Next
- Benchmark your own workload: Don’t trust anyone’s benchmark, including mine. Pull out the most-run queries in your system and test them on PostgreSQL 18.
- Check your cache hit rate: Below 95%, seriously consider direct connection. Below 80%, Redis is probably dragging you down.
- Do the full math: Redis infrastructure cost + engineering time on cache bugs + development velocity lost to cache logic. Once you run those numbers, the decision gets easy.
- Start with a low-risk endpoint: Pick a read-heavy, latency-tolerant endpoint. Dark launch it. Let the graphs speak.
Which endpoint in your system would be the best candidate to drop the cache layer first?
Next article, I’m planning to cover how PostgreSQL 18’s Virtual Generated Columns can eliminate certain “computed queries” that used to require caching. Follow along if that interests you.
References:
- pganalyze: Accelerating Disk Reads with Asynchronous I/O
- PlanetScale: Benchmarking Postgres 17 vs 18
- Phoronix: PostgreSQL 18.0 Released
- CYBERTEC: PostgreSQL 18 Better I/O Performance with AIO
- PVLDB 2026: io_uring for High-Performance DBMSs
- Neon: PostgreSQL 18 Asynchronous I/O
If this was useful, share it with a colleague who’s wrestling with the “do we need Redis?” question. Data beats arguments. Follow MengShou Coding for more hands-on database content.
To everyone who questioned the last article – is this enough data?
