During a code review meeting, I was about to explain our “carefully designed” caching architecture to a new intern when a message popped up on screen—our tech lead had dropped a screenshot in the group chat. It was a PostgreSQL 18 performance benchmark. I glanced at the numbers and froze. The intern leaned over, looked at it, and asked: “So… does this Redis cache layer still need to exist?”
The room went silent for about three seconds. Then all three of us opened our respective modules and started deleting code. 847 lines, 1,200 lines, 2,100 lines—over four thousand lines of caching logic, just gone. Nobody proposed it, nobody objected, nobody even felt the need to discuss it.
The Architecture I’d Been Guarding
Let me show you what our system looked like before:
┌─────────────────────────────────────────────────────────┐
│ Application │
└─────────────────┬──────────────────────┬────────────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Redis │ │ PostgreSQL │
│ (Cache) │ │ (Storage) │
│ ~0.1ms │ │ ~5-15ms │
└───────┬───────┘ └───────────────┘
│ ▲
└──────────────────────┘
(Cache invalidation nightmare)
I built this architecture myself. I defended it in design reviews, wrote documentation explaining why it was the “right” approach, and personally trained new hires on cache invalidation strategies.
I maintained this system for a long time. 847 lines of caching logic scattered across 12 files. Managed Redis cluster billing at $340/month. Cache invalidation bugs popping up every few days. Two monitoring dashboards to watch, two runbooks to memorize, and every time I got paged at 2 AM, I had to figure out whether it was Redis’s fault or PostgreSQL’s.
But honestly? I was kind of proud of this complex architecture. It made me feel like a real engineer solving real problems.
Then PostgreSQL 18 arrived.
The Librarian Finally Learned to Use Both Hands

PostgreSQL 18 introduced something called “Asynchronous I/O.” The official description reads: “A new I/O subsystem that can improve the performance of sequential scans, bitmap heap scans, vacuum operations, and more.”
Technically accurate, but sounds like reading from a spec sheet.
Let me translate that into human.
Old PostgreSQL was like a very proper librarian. You ask for a book, they walk to the shelf, pick it up, walk back, hand it to you. Then you ask for another book, they walk over, pick it up, walk back, hand it to you. One book at a time, very orderly, very slow.
New PostgreSQL? It’s like this librarian suddenly realized they have two hands. You ask for ten books, they grab them all in one trip. Done.
Technically, this is io_uring integration with the Linux kernel. In human terms: PostgreSQL finally learned to walk and chew gum at the same time.
The official claim was 2-3x performance improvement for read-heavy workloads. My reaction was: sure, keep dreaming. Database vendor benchmarks, you know how it is—everyone picks their best numbers for the slides. So I pulled our actual production queries and ran some tests to see what we were really dealing with.
Numbers That Made Me Question Reality

Our product dashboard has a query that loads a user’s activity data for the last 30 days. Nothing fancy—just aggregating events by date for a single user. This query runs a few million times per day in our system.
SELECT date, sum(events)
FROM activity
WHERE user_id = $1
AND date > now() - interval '30 days'
GROUP BY date;
Here are the test results:
PostgreSQL 17:
- Cold cache: 12-18ms
- Warm cache: 4-6ms
PostgreSQL 18 (with async IO enabled):
- Cold cache: 6-8ms
- Warm cache: 1.8-2.5ms
I ran it four times because I couldn’t believe my eyes.
Under 3ms with warm cache, querying 30 days of aggregated data. No Redis, no caching layer—just PostgreSQL doing its thing.
Now let’s do some uncomfortable math.
Yes, Redis cache hits are fast—0.1ms. Sounds great, right? But our hit rate was only 73%. That remaining 27% of cache misses meant 15ms queries plus the overhead of writing back to Redis, dragging down our average. Add in 0.3ms for complex object serialization, 0.8ms network round-trip to managed Redis, and those cache invalidation bugs that kept popping up—you can’t even put a number on that cost.
Weighted average of hits and misses: our “blazing fast” Redis cache had an effective average latency of about 4.2ms. PostgreSQL 18 without any caching layer? 2.1ms. Let that sink in.
Five Lines of Config That Changed Everything
Here’s what we changed in postgresql.conf:
-- Enable io_uring async IO
io_method = 'io_uring'
-- Let Postgres read concurrently
effective_io_concurrency = 200
maintenance_io_concurrency = 50
-- Allocate enough buffers (we have 64GB RAM)
shared_buffers = 16GB
-- Increase read-ahead merging
io_combine_limit = 512kB
That’s it. Five configuration options. No code changes, no architecture overhaul, no six-month migration project. After deployment, dashboard latency dropped 47%, and that’s when the scene from the beginning happened—three engineers silently started deleting cache code.
The Hidden Feature Nobody Talks About: Skip Scan
While everyone was focused on async IO, PostgreSQL 18 quietly shipped another feature that was even more important for us: B-tree skip scan.
We have an orders table with a composite index on (user_id, status, created_at). Standard stuff, nothing special.
But we also have queries like this:
SELECT * FROM orders
WHERE status = 'pending'
AND created_at > '2025-01-01'
ORDER BY created_at DESC
LIMIT 50;
Notice the problem? No user_id filter.
PostgreSQL 17 couldn’t efficiently use our composite index for this query. The database would either do a full table scan or take a suboptimal path. Our solution was to cache the results in Redis with a 60-second TTL.
PostgreSQL 18 can now “skip” over the first column of the index. It performs a series of range scans for each distinct user_id value, using the index even when the first column isn’t specified in the WHERE clause.
Our order status query went from 45ms to 8ms.
No cache needed, no TTL management, no invalidation logic. Just the PostgreSQL team making the database better.
The Real Cost of Caching

I sat down and calculated what our Redis cache layer actually cost us. Not just infrastructure—everything.
Managed Redis infrastructure: $340/month, over $10,000 accumulated.
Engineering time on cache-related bugs: I checked our issue tracker—over 20 cache-related incidents, averaging 7 hours to resolve. At a conservative $150/hour, that’s another $20,000+ in engineering time.
Slower feature development: Every new feature required thinking about cache invalidation. Every schema change meant updating cache keys. Every deployment needed cache warming strategies. I estimate this added 15% overhead to development velocity. Four engineers over time—that’s thousands of hours.
Cognitive load: This one’s hard to quantify. But every engineer on the team, during every code review, every architecture decision, every debugging session, had to keep cache invalidation logic in their head. That mental burden has a cost.
Conservative estimate: over $36,000 spent on infrastructure that PostgreSQL 18 replaced with a kernel feature and five lines of config.
This isn’t just a technical lesson. It’s a business lesson about the real cost of unnecessary complexity.
Redis Still Has Its Place
Let me be clear: I’m not saying Redis is dead. I’m not saying you should delete all your Redis code tomorrow. I’m not writing an obituary for in-memory caching.
What I am saying is: Redis as a read cache for PostgreSQL queries is becoming optional in many scenarios.
Here’s where Redis still wins:
Session storage: When you need sub-millisecond response with zero variance. User sessions must respond instantly. Even with async IO, PostgreSQL can’t match Redis for pure key-value lookups.
Rate limiting: Atomic increment operations with auto-expiration. INCR with TTL is Redis’s bread and butter. This isn’t a caching use case—it’s a data structure use case.
Pub/Sub: Real-time event distribution across multiple consumers. PostgreSQL has NOTIFY, but Redis pub/sub is more mature and scales better for high-throughput messaging.
Leaderboards and sorted sets: Redis’s ZADD and ZRANGE operations are purpose-built for ranking. PostgreSQL can do it with window functions, but Redis does it more elegantly.
Distributed locks: The SETNX pattern for distributed locking is battle-tested and reliable. This is infrastructure, not caching.
We still run Redis in production for session storage and rate limiting. We just don’t use it for query caching anymore.
Here’s what our architecture looks like now:
┌─────────────────────────────────────────────────────────┐
│ Application │
└─────────────────┬──────────────────────┬────────────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Redis │ │ PostgreSQL 18 │
│(Sessions/Rate)│ │ (Everything) │
└───────────────┘ └───────────────┘
Simpler, cheaper, fewer incidents, fewer dashboards, less to explain to new hires.
When This Won’t Work
I’m not going to pretend this approach works for everyone. Let me be clear about the constraints.
Latency requirements: If your P99 needs to be under 2ms, PostgreSQL 18 alone might not cut it. True sub-millisecond guarantees still require in-memory solutions.
Traffic scale: If you’re handling millions of requests per second hitting the same hot keys, Redis is still the right choice. Connection overhead alone would crush PostgreSQL.
Kernel version: io_uring support requires Linux kernel 5.10 or newer. If you’re running on older infrastructure, you won’t get the async IO benefits.
Dataset size: If your working set doesn’t fit in memory and you’re I/O bound on spinning disks, async IO helps but isn’t magic. SSDs are still essential for this approach.
Cloud provider limitations: Some managed PostgreSQL services don’t yet support io_uring configuration. Check with your provider before migrating.
Be honest about your actual requirements. Measure your actual latency. Look at your actual hit rates.
The Uncomfortable Question
After deleting those 847 lines of code, one question kept bouncing around my head:
How many engineering decisions in my codebase exist because “that’s what experienced engineers do” rather than “we measured and this is faster”?
I added Redis caching on day one of the project because that’s what seasoned developers do. We cache by default, treat the database as slow unless proven otherwise. We build complexity because complexity looks professional.
The result? I was maintaining infrastructure that solved a problem I never actually measured.
The PostgreSQL team spent years building async IO. They rewrote core parts of how the database talks to storage. They integrated bleeding-edge Linux kernel features. They ran thousands of benchmarks and fixed hundreds of edge cases.
And all that work exposed my 847 lines of caching code for what it really was: premature optimization based on assumptions I never validated.
One Last Question
Every codebase has decisions that made sense at the time but have outlived their context. Every architecture has complexity that exists because someone assumed it was necessary, not because they proved it.
PostgreSQL 18 forced me to confront one such assumption in my own work. The cache layer I defended in code reviews, documented in wikis, and taught to new hires was solving a problem that stopped existing the moment the PostgreSQL team shipped better I/O handling.
I don’t know what assumptions are hiding in your codebase. I don’t know which complexity is load-bearing and which is legacy from a different era. I don’t know if your Redis cache is essential or optional.
But I do know that measuring is the only way to find out.
I deleted 847 lines of code because I finally measured.
Maybe you should too.
If you found this useful, give it a like and share. Got friends wrestling with caching architecture? Send them this—might save them some money and headaches. Follow me for more war stories from the trenches.