sanchescom / laravel-cache-memory
Laravel cache driver backed by System V shared memory (shmop) with semaphore-guarded atomic operations.
Package info
github.com/sanchescom/laravel-cache-memory
pkg:composer/sanchescom/laravel-cache-memory
Requires
- php: ^8.2
- ext-shmop: *
- ext-sysvsem: *
- illuminate/cache: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^9.9|^10.0
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.0
Suggests
- ext-pcntl: Required to run the multi-process concurrency test group.
- ext-posix: Required by the concurrency test group's watchdog to kill an unresponsive child.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 13:48:21 UTC
README
A Laravel cache driver backed by System V shared memory (shmop). Data
survives across PHP-FPM/CLI worker requests on the same host without
standing up Redis or Memcached — useful for small, hot, single-box caches
(rate-limit counters, feature-flag snapshots, warm lookup tables) where an
external cache service would be overkill.
Table of Contents
- Requirements · Installation · Quick Start
- Configuration · How It Works · Atomicity
- Limitations · Testing Your Application
- Roadmap · Upgrading · Contributing
- Versioning · Authors · License
Requirements
- PHP 8.2+
ext-shmopext-sysvsem- Laravel 11 or 12 (
illuminate/cacheandilluminate/support^11.0|^12.0)
Note
Laravel 11 reached end of life in 2026 (no more security fixes). The package still supports and tests it, but new projects should target Laravel 12.
Installation
composer require sanchescom/laravel-cache-memory
The service provider (Sanchescom\Cache\MemoryServiceProvider) is
registered automatically via package auto-discovery — no manual
registration needed.
Quick Start
Add a memory store to config/cache.php:
// config/cache.php 'stores' => [ // ... 'memory' => [ 'driver' => 'memory', 'key' => env('MEMORY_BLOCK_KEY', 771055301), // pick your OWN value — see below 'size' => env('MEMORY_BLOCK_SIZE', 320000), // bytes; omit to use the 320000-byte default ], ],
Change that key. System V IPC keys are a single host-wide namespace
shared by every process on the machine, so two applications configured
with the same key attach to the same segment and silently read and
overwrite each other's cache entries — pick a distinctive value per
application (and per environment, if several run on one host) rather than
copying the one above.
Then use it like any other Laravel cache store:
use Illuminate\Support\Facades\Cache; // One process writes... Cache::store('memory')->put('some_key', ['value' => 'text'], 60); // 60-second TTL // ...another process on the same host reads it back. $data = Cache::store('memory')->get('some_key'); // Counters are exact under concurrent access (see Atomicity below). Cache::store('memory')->increment('hits');
Configuration
Each configured store gets its own isolated shared memory segment and its
own semaphore — two stores never share state, even if both use the
memory driver.
| Key | Type | Default | Notes |
|---|---|---|---|
driver |
string | — | Must be memory. |
key |
int|null | derived via ftok() |
SysV IPC key. Cast to int by the provider. See the [!WARNING] below about leaving this unset in production. |
size |
int|null | 320000 (MemoryBlock::DEFAULT_SIZE) |
Total segment size in bytes, including a 4-byte internal header. Cast to int by the provider. |
If key is omitted, MemoryBlock derives one from ftok(__FILE__, 'b')
— i.e. from the installed package file's device and inode, not from
anything you configure. If size is omitted, the segment is sized at
MemoryBlock::DEFAULT_SIZE (320,000 bytes).
How It Works
- Shared memory segment. Each store owns one
shmopsegment sized bysize(or the 320,000-byte default). All keys for that store — the entire cache — live serialized inside this one fixed-size segment. - Length-prefixed framing. A write packs a 4-byte big-endian length
header (
pack('N', strlen($payload))) followed by the serialized payload. Reads trust that header rather than scanning for a terminator, which is what makes a read of a segment mid-write detectable instead of silently truncated. - One semaphore per store, paired to the segment's key. The store's
constructor calls
sem_get($key, 1, 0644, false)against the same key used for the shared memory segment — the same permissions as the segment itself, so the lock is no easier to grab than the data it guards. Every public operation on the store —get()included — acquires this semaphore before touching the segment and releases it afterward. - Garbage collection on overflow. When a write no longer fits, expired
entries are purged first and the write is retried. If it still doesn't
fit, the whole cache for that store is written back empty into the
same segment and an
E_USER_WARNINGis raised, in that order (see Limitations — this is destructive and intentional, not a bug). The segment itself is never deleted on this path; onlyrequestDeletion()does that.
Atomicity
Every store operation — reads included — runs inside the
semaphore-guarded critical section described above, which is what makes
increment()/decrement() exact under concurrent access instead of
merely "usually correct." This is proven by a Pest test that forks 16
child processes, each incrementing the same counter 50 times, across 6
independent rounds, and asserts the final count equals exactly
16 × 50 every round. It's a real net, not a rubber stamp: run against a
deliberately reverted version of the locking fix, it caught the
regression in 40 out of 40 runs.
Note
That test has been verified repeatedly on macOS during development.
CI (.github/workflows/ci.yml) is this package's first data point on
Linux — nothing here has been separately verified on Linux outside CI.
Limitations
Warning
Fixed segment size. The segment never grows. When a write would
exceed the configured size, expired entries are garbage-collected
first. If the write still doesn't fit, the entire store is written
back empty and only then is an E_USER_WARNING raised. The order
matters, because that warning does not behave the same everywhere:
- Inside a booted Laravel application,
HandleExceptionspromotesE_USER_WARNINGto a thrownErrorException, so the overflowing call does not return —Cache::put()throws, and your exception handler (and your logs) see it. - Outside Laravel — plain PHP, or any handler that swallows the
warning — the same call returns normally, and
put()returnstrue.
The state left behind is identical in both worlds: the store is empty and immediately usable again. The value you just stored, and everything else in that store, is gone. Size the store for your real working set, and treat this warning as a sizing bug in your configuration, not as routine cache pressure.
Warning
requestDeletion() disconnects other live processes from the cache.
Deleting and recreating a segment produces a new segment under the
same key. Any other process that already holds an open handle keeps
reading and writing the old, orphaned one for the rest of its life: it
only reopens when the key has no segment at all, and it has one again
immediately. Both sides still share the same semaphore, so their
accesses stay perfectly serialised while their data diverges — which
looks exactly like a locking bug and is not one. Call
requestDeletion() only when nothing else is using the store, and
restart your workers afterwards. (This is why the overflow path above
empties the segment instead of recreating it.)
Warning
Cache::add() is not atomic on this driver. MemoryStore
implements Illuminate\Contracts\Cache\Store and has no add() method
of its own, so Repository::add() falls back to a non-atomic
get() + put() — two processes can both observe the key as missing
and both write. increment()/decrement() are atomic (see
Atomicity); add() is not, and neither is
Cache::lock(), which this driver does not support at all. Both are
2.1 candidates.
Warning
Data is lost on reboot, or whenever the segment is deleted. Shared
memory is not persistent storage — it does not survive a host restart,
and both requestDeletion() and the overflow path above discard
everything the store held.
Warning
POSIX only — no Windows support. ext-shmop and ext-sysvsem are
System V IPC facilities; there is no Windows shmop/sysvsem
implementation to fall back to.
Warning
The default key is not stable across symlinked deploy paths. With
key left unset, the segment's SysV key comes from ftok(__FILE__, 'b')
— derived from the installed package file's device and inode. A
symlink-swap deploy layout (e.g. Capistrano-style releases/<n> +
current symlink) copies fresh files into a new release directory on
every deploy, giving that file a new inode and therefore a new default
key each time you deploy — silently disconnecting old worker processes
from new ones. Set an explicit key in production.
Warning
Segment and semaphore permissions are 0644 — same-user processes
only. Both the shared memory segment and its paired semaphore are
created owner-read-write, group/other-read-only. Only processes running
as the same user that created them can write to the segment or acquire
the lock; processes running as a different user can, at best, read the
segment. There is currently no way to configure this.
Additionally: the paired semaphore is not removed by
requestDeletion() or MemoryBlock::delete() — only the shared memory
segment is. This is deliberate (see UPGRADE.md), but it
means a long-lived host that cycles through many distinct keys
accumulates one leftover semaphore per key. Reuse a stable, small set of
keys.
Testing Your Application
For most applications, the right thing to do in tests is not to use
the memory driver at all — set CACHE_STORE=array (or configure the
array store directly) so tests don't touch real shared memory or leak
SysV resources between runs.
If you specifically need to exercise the memory driver:
- Give each test (or test process) its own unique
key, so parallel test runs never collide on the same segment. - Call
Cache::store('memory')->getStore()->requestDeletion()in teardown to drop the segment. Remember this leaves the semaphore in place (see Limitations) — if your test suite forks processes or runs many times against a fixed key, clean the semaphore up separately (sem_get($key)+sem_remove($semaphore)). - Also call
Cache::purge('memory')afterrequestDeletion()so the resolved repository (and theShmophandle it holds) is dropped too — deleting the segment does not detach your process from it, and a kept handle burns a SysV attachment slot per test until you hit "Unable to open shared memory segment". This package's own feature suite does exactly this in itsafterEach().
Roadmap
See ROADMAP.md for planned features (an atomic add(), a
Cache::lock() provider on SysV semaphores, segment usage stats,
configurable permissions, and an APCu fallback driver).
Upgrading
See UPGRADE.md for breaking-change notes, including the 1.x → 2.0 migration.
Contributing
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
Versioning
We use SemVer for versioning. For the versions available, see the tags on this repository.
Authors
- Efimov Aleksandr - Initial work - Sanchescom
See also the list of contributors who participated in this project.
License
This project is licensed under the MIT License - see the LICENSE.md file for details.