Sitemap
Level Up Coding

Coding tutorials and news. The developer homepage gitconnected.com && skilled.dev && levelup.dev

🧩 Caching ≠ Redis: Why We Built Our Own Database Cache

4 min readOct 6, 2025

--

Press enter or click to view image in full size

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:

  1. Take the request body → generate a hash (unique key).
  2. Look for that hash in req_keeper.
  • âś… If it exists → return the cached JSON response.
  • ❌ If not → process the request, save the response inreq_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:

  1. Application-level caching (our req_keeper table)
  2. 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:

  1. TTL (30 minutes) → cached entries expire automatically.
  2. Invalidation triggers → any changes in key tables (company_products, variants, rules, etc.) delete related cache rows.
  3. 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?

--

--