🧩 Caching ≠Redis: Why We Built Our Own Database Cache
Be honest — what’s the first tool that comes to your mind when someone says “caching”?
If you’re like most engineers, the answer is probably: Redis.
And I don’t blame you. Redis is fast, reliable, and battle-tested. But here’s the thing:
👉 Not every caching problem needs Redis.
We ran into this when one of our APIs was handling almost half of all traffic. At first, our instinct was the same: “Let’s add caching, maybe Redis?” But the deeper we thought about it, the more we realized Redis wasn’t the right fit.
Here’s how we ended up building a lean caching layer directly in our database — and why it worked better than adding more infra.
🚨 The Pain You Might Recognize
Every system has that one API that everyone calls too much. For us, it was the list-product API.
- It handled nearly half of all incoming requests
- Each request involved heavy work — aggregating product and variant data
- And the worst part? No caching at all
That meant every repeated request forced the server to recompute the same thing, over and over again. Wasteful. Slow. Costly.
Sound familiar?
🤔 The Big Question: Do We Really Need Redis?
Our first reaction was obvious: “Let’s add Redis — that’ll make responses faster.”
But then we stopped to think:
- Do we really want to manage another service?
- Are we ready for scaling, backups, and monitoring overhead?
- Can we justify the monthly cost of running a Redis cluster?
The more we thought about it, the clearer it became: Redis would solve the problem, but at too high a cost.
And here’s the kicker: we were already running PostgreSQL + Hasura Enterprise, which has its own caching layer.
So instead of adding Redis, we asked a new question:
👉 Could we build caching directly into our database?
💡 The “Aha!” Moment
That’s how req_keeper was born — a simple table inside Postgres acting as our caching layer.
The idea was straightforward:
- Take the request body → generate a hash (unique key).
- Look for that hash in
req_keeper.
- ✅ If it exists → return the cached JSON response.
- ❌ If not → process the request, save the response in
req_keeper, then return it.
No new infra. No new complexity. Just smarter use of the tools we already have.
đź“„ The Cache Table
CREATE TABLE IF NOT EXISTS public.req_keeper (
req_keeper_pk UUID PRIMARY KEY DEFAULT gen_random_uuid(),
company UUID REFERENCES public.companies(company_pk) NOT NULL,
hash VARCHAR(256) NOT NULL, -- Request body hash
type VARCHAR(256) NOT NULL, -- Request type (e.g., list-product)
data JSONB NOT NULL, -- Cached API response
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);⚡ Using It with Hasura
Here’s where things got even better. Since we were already using Hasura Enterprise, we could layer its built-in caching on top.
Get Shubham Maurya’s stories in your inbox
Join Medium for free to get updates from this writer.
Hasura internally uses Redis to cache database queries — but we didn’t need to manage Redis ourselves. By simply enabling Hasura’s caching features, we got:
- Fine-grained caching at the query level
- The ability to set TTL (time-to-live) per query
- Automatic invalidation when underlying data changes
Here’s an example query using Hasura’s @cached directive:
const query = `
query getReqKeeper @cached(ttl: 120) {
req_keeper(
where: {
type: { _eq: "${type}" }
hash: { _eq: "${hash}" }
company: { _eq: "${companyId}" }
}
order_by: { created_at: desc }
limit: 1
) {
data
created_at
}
}
`This meant we had two layers of caching working together:
- Application-level caching (our
req_keepertable) - Query-level caching (via Hasura + Redis under the hood)
Best part? We didn’t have to manage Redis directly.
🔄 But Wait — What About Stale Data?
Caching is great — but only if the data stays fresh.
To keep things reliable, we designed a 3-part freshness strategy:
- TTL (30 minutes) → cached entries expire automatically.
- Invalidation triggers → any changes in key tables (
company_products,variants,rules, etc.) delete related cache rows. - Cron cleanup → any leftover cache entries older than 1 week are purged.
This gave us the confidence that users always saw fast yet accurate responses.
🚀 The Benefits
Here’s what we gained by avoiding Redis:
- ⚡ Faster responses for repeat requests
- 📉 Lower server load (no duplicate heavy queries)
- đź’° Cost savings (no Redis infra bills)
- 🛠️ Simplicity (everything runs inside Postgres + Hasura)
Sometimes, not adding new infra is the smartest engineering choice.
🔑 Lessons Learned
- Caching ≠Redis → don’t assume it’s always the default solution.
- Freshness > Speed → invalidation strategies matter more than raw performance.
- Leverage what you already have → our stack (Postgres + Hasura) was enough.
🌟 Wrapping Up
Not every caching problem needs Redis. Sometimes the smartest move is asking: “Can I solve this with what I already have?”
For us, the answer was yes — and that choice turned a bottleneck into a win without adding extra infra.
I’d love to hear from you: where have you implemented caching outside of Redis — and what worked best in your setup?











