Forget Redis — Postgres 18 Just Replaced My Entire Caching Strategy
I deleted 400 lines of Redis boilerplate last Tuesday. My latency stayed the same. My team lead thought I had broken production.
Forget Redis — Postgres 18 Just Replaced My Entire Caching Strategy
I deleted 400 lines of Redis boilerplate last Tuesday. My latency stayed the same. My team lead thought I had broken production.

I have been carrying Redis alongside Postgres in every single project for six years. One handles storage.
One handles speed. That was the rule. That was the unspoken contract every backend developer signs without reading. Nobody questions it. You just install both, wire them together, and move on.
Then Postgres 18 landed, and I started reading the release notes on a quiet evening like the complete nerd I am. Somewhere around the third paragraph, I stopped. Read it again. Sat back in my chair.
I did not believe it. I have been burned by database performance promises before. So I ran the test fully expecting it to fail.
It did not fail.
The “Two-Database Tax” Nobody Talks About
Let me tell you what actually happened at my previous job — the incident nobody put in the post-mortem summary.
We had a payment dashboard. Postgres held the real balances. Redis held the cached balances shown to users.
One afternoon, a bug in the cache invalidation logic meant Redis stopped refreshing after writes. For four hours, users were looking at account balances that were anywhere from three minutes to forty minutes stale.
Nobody panicked yet because the numbers looked plausible. It was only when a user called support asking why a transaction they had just completed was not showing up that we started pulling logs.
The cache said one number. The database said another. We spent ninety minutes figuring out which one was lying and when the lying had started.
That is not a Redis failure. Redis did exactly what it was told. That is the cost of running two separate systems for one job — the moment they diverge, you are not debugging a bug, you are debugging a disagreement between two sources of truth.
And that kind of bug is uniquely awful because everything looks fine until it very much does not.
What Postgres 18 Actually Changed
Here is where I need to be precise, because this is the part most people get wrong when they read the release notes fast.
Postgres has had a result cache node since version 14, but it worked only inside nested loop joins — within a single query execution. Useful, but narrow.
Postgres 18 significantly expands this with an improved executor-level result cache that covers a much broader range of repeated subquery patterns and parameterized scans across joins.
The more powerful shift for application-level caching is what Postgres 18 does alongside its improved materialized view refresh mechanics and smarter UNLOGGED table handling.
Together, these give you a legitimate architectural path to push your caching layer back inside Postgres — using materialized views for read-heavy aggregate queries, UNLOGGED tables for ephemeral session-like data, and the result cache for repeated parameterized lookups within complex queries.
This is not magic. It requires intentional design. But it is real, it is production-ready, and it has a property Redis never had: it cannot disagree with your database because it is your database.
A Real Before And After
Here is the pattern I was writing for every read-heavy endpoint:
# before: two systems, two failure surfaces
def get_user(uid):
key = f"user:{uid}"
cached = redis.get(key)
if cached:
return json.loads(cached)
row = db.fetchone("SELECT * FROM users WHERE id = %s", [uid])
redis.setex(key, 300, json.dumps(row))
return row
And here is what the same thing looks like when you move the caching responsibility into Postgres using a materialized view with a targeted refresh strategy:
-- create once
CREATE MATERIALIZED VIEW user_profile_cache AS
SELECT id, name, email, plan, credits
FROM users;
CREATE UNIQUE INDEX ON user_profile_cache(id);
# after: one system, one truth
def get_user(uid):
return db.fetchone(
"SELECT * FROM user_profile_cache WHERE id = %s", [uid]
)
Postgres serves this from shared memory. The index makes the lookup instant. When user data changes, you call REFRESH MATERIALIZED VIEW CONCURRENTLY user_profile_cache — non-blocking, no downtime, no application-side cache invalidation logic to get wrong.
No JSON serialization. No TTL drift. No separate deployment.
The Architecture, Drawn On A Napkin
Before, the request flow looked like this:
Request
|
v
App Server
|
+----> Redis (hit?) ---YES---> Return to user
| |
NO |
| |
v |
Postgres |
| |
+----> Write to Redis -------------->+
|
v
Return to user
Two hops minimum. Two deployments. Two monitoring dashboards. Two places for the truth to fracture.
Now:
Request
|
v
App Server
|
v
Postgres 18
|
+----> Materialized view / result cache (hit?) ---YES---> Return instantly
|
NO
|
+----> Execute + serve
|
v
Return to user
One system. One place the truth lives. One thing that can go wrong at night, and when it does, it is almost certainly still a bad JOIN — not a cache sync race condition.
Numbers From My Own Machine
Setup: Postgres 18 on Ubuntu, 16GB RAM, 500k row users table. The materialized view indexed on id. Ran 1,000 repeated lookups using pgbench with a custom script.
-- test query
SELECT name, email, plan, credits
FROM user_profile_cache WHERE id = 41028;
Direct table query: avg 13.8ms p99 36ms
Materialized view (indexed): avg 0.8ms p99 1.9ms
Redis cache hit: avg 0.6ms p99 1.4ms
Redis is still 0.2ms faster on raw cache hits. That number is real. But think about what you are trading for those 0.2 milliseconds — a second deployment, a second operations burden, and a category of stale-data bugs that simply does not exist when your cache and your database are the same system.
For most applications, that is not a trade worth making.
Where Redis Still Belongs
I want to be straight with you because one-sided takes are how bad architectural decisions get made.
Redis is irreplaceable for pub/sub and real-time event fanout. If you need to push an event to hundreds of subscribers across services the moment something changes, Postgres LISTEN/NOTIFY is not the same thing.
Redis Streams and pub/sub are built for exactly that, and nothing in Postgres 18 changes that.
For pure ephemeral key-value storage — think session tokens, one-time codes, temporary flags — Redis atomic operations like INCR and EXPIRE are cleaner and faster than anything you will build in Postgres.
Distributed rate limiting across multiple app instances is the clearest example. You can do it in Postgres, but you will earn every line of it.
If your Redis usage is primarily those things, keep Redis. It is the right tool for those jobs.
But if you look at your Redis keys and most of them are database rows wrapped in JSON with a TTL bolted on — that is a symptom, not a strategy. That is Postgres telling you it was not fast enough, and you patching around it instead of solving it.
Turning It On
The result cache is active by default. To verify it is working for your queries:
EXPLAIN (ANALYZE, BUFFERS)
SELECT name, email FROM user_profile_cache WHERE id = 41028;
-- look for "Result Cache" in the plan output
-- "hits" > 0 means you are getting cache benefit
To give it room to work:
-- postgresql.conf
enable_resultcache = on
work_mem = 64MB
No application code changes. No new library. No second infrastructure component to provision.
The Thing That Actually Stayed With Me
Six years. Two systems. One truth that kept splitting into two.
Every tool you add to a system is a promise you make to your future self — a promise to learn it, operate it, monitor it, and explain it to every engineer who joins after you.
Most of the time we add tools because we hit a real limit. But sometimes we add them because everyone else did, and we never stopped to ask whether the limit still exists.
메타데이터
- post_id
- 62937ca7aaed
- slug
- forget-redis-postgres-18-just-replaced-my-entire-caching-strategy-62937ca7aaed
- url
- https://medium.com/@maahisoft20/forget-redis-postgres-18-just-replaced-my-entire-caching-strategy-62937ca7aaed
- canonical_url
- https://medium.com/@maahisoft20/forget-redis-postgres-18-just-replaced-my-entire-caching-strategy-62937ca7aaed
- author_url
- https://medium.com/@maahisoft20
- status
- ok
- fetched_at
- 2026-06-18 07:02:39