Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Hybrid Cache Laravel Package

rajmundtoth0/hybrid-cache

A Laravel package that adds a hybrid cache layer, combining fast in-memory storage with persistent backends to reduce latency and database load. It offers simple cache APIs and configurable drivers for scalable PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require rajmundtoth0/hybrid-cache
php artisan vendor:publish --tag=hybrid-cache-config   # check the actual tag in the package README

Look at these three things first, in order:

  1. The published config file — this is where the L1 (in-process) and L2 (persistent) layers are wired together. Confirm which Laravel cache store backs the L2 layer and what the L1 TTL/size limits are. Everything else in the package is downstream of this file.
  2. composer.json constraints — 0.2.0 targets Laravel 13. If the app is on 11.x or 12.x, pin ^0.1.0 and revisit at upgrade time. This is the single most important compatibility fact in the release.
  3. The service provider bindings — check whether it swaps the default cache driver, registers a new store driver ('driver' => 'hybrid'), or provides a decorator you opt into. That determines whether adoption is global or per-store.

First use case: pick one hot, read-mostly, small-payload path and move it onto the hybrid store. Good candidates: feature flags, tenant settings, currency/tax tables, permission maps, small reference data resolved on nearly every request. The win is that repeated reads inside a single request (or a single Octane worker cycle) skip the Redis/DB round trip entirely. Do not start with your session store, queues, or anything holding large blobs.

A minimal adoption looks like defining a dedicated store rather than replacing default:

// config/cache.php
'stores' => [
    'hybrid' => [
        'driver' => 'hybrid',
        // L1 + L2 settings come from config/hybrid-cache.php
    ],
],

Then Cache::store('hybrid')->remember(...) in the one place you're optimizing, and measure before widening.

Implementation Patterns

Per-request memoization for expensive lookups. The core pattern the package earns its keep on: wrap a call that is cheap to recompute but expensive to fetch.

$rates = Cache::store('hybrid')->remember("fx:rates:{$base}", now()->addHour(), fn () =>
    $this->fxService->fetch($base)
);

Called ten times in a request, the L2 is hit once at most and the L1 thereafter. This is the highest-value, lowest-risk integration and where most teams should stop.

Sliding TTL with touch (new in 0.2.0). touch() extends a key's lifetime without reading and re-serializing the value. Use it where you want "keep this warm while it's being used" semantics:

if (Cache::store('hybrid')->has($key)) {
    Cache::store('hybrid')->touch($key, now()->addMinutes(30)); // no value fetch, no re-write
}

This is meaningfully cheaper than the old get()-then-put() idiom, and it's the reason to upgrade if you're already on the package. Verify the touch propagates to both layers in your config — if the L1 and L2 TTLs diverge, touch on a store that only forwards to L2 leaves you with a cold L1 and a hot L2, which is correct but not what people usually assume.

Read-through for reference data with explicit warmup. For tables that change on deploy or via admin action, prefer:

  • A console command / seeder that pre-populates the L2 keys at boot or after deploy.
  • Normal remember() reads at request time.
  • Explicit forget() in the model observer or admin action that mutates the data.

This keeps invalidation deterministic instead of relying on TTL expiry to paper over bugs.

Testing. Use Cache::spy() / Cache::fake() against the hybrid store like any other store; assert on remember and touch calls rather than on internal L1 state. For integration tests, clear the L1 between test cases — a shared in-process array surviving across tests in the same PHPUnit process is a classic source of order-dependent failures.

Queue workers and Octane. Long-lived processes are where hybrid caching actually shines (one L1 warm across hundreds of jobs) and where it bites hardest. Treat L1 as request/worker-scoped, keep TTLs short (seconds to a couple of minutes for mutable data), and never cache authorization decisions or anything user-specific in a worker that serves multiple users.

Gotchas and Tips

L1 staleness is the whole trade-off, and it isn't abstract. With N app servers or N queue workers, an invalidation on one process does not reach the others' in-memory layers. For data with an SLA on freshness — pricing, entitlements, anything an admin expects to change instantly — either keep the L1 TTL very short, disable L1 for that key class entirely, or route writes through a broadcast/pub-sub invalidation channel if the package offers one. Confirm what the package actually guarantees here before you rely on it.

Laravel 13 floor. 0.2.0 is a hard bump. Check illuminate/* constraints, and check whether the cache touch contract it now implements exists in your installed Laravel version — that's almost certainly why the major-ish constraint moved. Plan the package upgrade and the framework upgrade as one unit of work, not two.

Memory growth in long-lived workers. An unbounded L1 array is a slow leak in Octane/queue contexts and a fast one if you cache variable-cardinality keys (per-user, per-tenant, per-query-string). Set the size cap in config and prefer keys with a bounded keyspace.

Serialization and payload size. The L1 layer typically holds values deserialized in memory. Large arrays, Eloquent models with loaded relations, and closures all behave differently across the two layers than they do on a plain Redis store. Cache scalars and small DTOs; store IDs and re-query for models.

Atomicity and locks don't come for free. Cache::lock() and atomic increment semantics are L2 concerns. Don't assume the hybrid store preserves them; if you need locks, point them at a concrete store explicitly.

Debugging checklist when a stale value appears:

  1. Is it in L1 only? Restart the worker / php artisan octane:reload — if the stale value vanishes, your invalidation isn't crossing processes.
  2. Is the key actually being written to L2? Inspect the underlying store directly (e.g. redis-cli KEYS, or Cache::store('redis')->get($key)).
  3. Does forget() clear both layers? Verify rather than assume.
  4. Is the TTL being extended by touch in a place you forgot about?

Config quirks to pin down early: which store is the L2 default, whether TTLs are defined per-layer or once, whether the package respects Cache::tags() (many decorator-style stores don't — test it before designing around tags), and whether the prefix from config/cache.php applies to both layers or only L2.

Extension points. The useful seams are the L2 store binding (swap Redis for database in local dev so you don't need Redis running) and the key-prefix/TTL resolution if the package exposes them. Wrap the package behind your own thin App\Support\HotCache facade if you expect to swap it out — the API surface is small enough that the indirection costs little and saves a wide refactor later.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource