Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

kai-init's avatar

I built a automatic normalized caching layer for Eloquent — massive Redis memory savings

Hey everyone 👋

Over the past few months, I’ve been working on a small open-source package called laravel-normcache.

It actually started from a problem I kept running into across a few different projects. I found myself repeatedly building the same kind of caching layer to avoid redundant Eloquent data sitting everywhere in Redis, so eventually I thought: why not turn it into a reusable package and learn more deeply how Laravel caching works under the hood?

A lot of the ideas are inspired by patterns used in things like Redux/Apollo normalized stores, and I’ve been experimenting and refining things as I use it across real projects.

The main idea behind laravel-normcache is normalizing cached Eloquent results instead of storing entire query collections repeatedly. It stores individual models and query ID maps separately, so when one model changes, every cached query containing that model automatically reflects the update.

Query cache  →  "posts where active=1, page 2"  →  [4, 7, 12]
Model cache  →  post:4  →  { id:4, title:..., body:... }
             →  post:7  →  { id:7, title:..., body:... }
             →  post:12 →  { id:12, title:..., body:... }

Some things it currently does:

  • Automatic caching + invalidation with just a single trait
  • Avoids storing duplicate model data across query caches
  • Instant consistency across cached query results
  • O(1) invalidation using versioned cache keys
  • Uses dramatically less Redis memory compared to traditional query caching approaches

There’s still plenty I want to improve and optimize over time, but I felt it was finally in a good enough place to share with the community.

Here’s a quick benchmark comparing direct DB queries vs laravel-model-cache for reference:

benchmark

I’d genuinely appreciate any feedback, criticism, ideas, or contributions from people more experienced than me. I’m mainly here to learn and improve as an open-source developer.

Repository: https://github.com/kai-init/laravel-normcache

Thanks for reading 🙏

1 like
4 replies
imrandevbd's avatar

Hey Kai, really solid concept here. Dealing with Redis memory bloat from duplicate query collections is a massive headache, especially on some of the larger Laravel apps I manage where tables easily hit 60M+ records. Bringing the Redux/Apollo normalized store pattern to Eloquent is a brilliant approach to solving that.

Quick question on the architecture: how are you handling eager loaded relationships (with())? Does the package normalize the nested models and just store their IDs on the parent, or are they embedded directly within the parent model's cached payload?

kai-init's avatar

Hi @imrandevbd That’s a really solid question. It's fully normalized. I made a conscious choice to never embed child models.

The eloquent builder override somewhat handles normlization by default. It intercepts the standard eager-loading flow and routes it through a few different strategies depending on the relation.

Basically anything but a BelongsTo relation gets a seperate query cache for the list of models to load.

  • BelongsTo: Since we already has the child IDs, It skip the query cache entirely and just do a batch MGET against the entity store.
  • HasMany: It intercept that whereIn query and cache just the child IDs in their own query cache (query:{child}:v{v}:{hash}).
  • Many-to-Many: These get a specialized "pivot cache" so It can map the IDs while keeping the unique join-table data (the pivot attributes) normalized (query:{child}:v{v}:{hash})
  • Through: Similar logic for Through relations, it bridge the distant models with a "through cache" that's version sensitive to the target and the bridge model.
imrandevbd's avatar

That MGET strategy for BelongsTo is definitely the right call saves a ton of overhead compared to standard query caching.

I’m curious about the hydration side of things. When you're pulling a large collection and stitching all those normalized models back together, have you noticed much of a CPU hit on the PHP side? Reconstructing Eloquent models in a loop can get pricey if the collection is huge.

Also, how are you handling count() or exists()? Do those get their own entries in the query map, or are they just bypassing the cache for now?

Jessehilton's avatar

This is a really impressive approach to optimizing Laravel caching. Normalizing cached Eloquent data instead of repeatedly storing full query collections sounds like a smart way to improve cache efficiency, consistency, and scalability across large applications. I also like how the package takes inspiration from Redux and Apollo patterns — it makes the architecture feel much more modern and maintainable for real-world Laravel projects.

Please or to participate in this conversation.