A Laravel cache driver backed by System V shared memory (shmop). One cache
segment on the host, shared by every PHP process on it: PHP-FPM workers,
artisan commands, queue workers. No Redis, no Memcached, no socket. Meant
for small, hot, single-box caches (rate-limit counters, feature-flag
snapshots, warm lookup tables) where an external cache service is overkill.
Atomic increment(), decrement() and add() (with a TTL), and
Cache::lock() without Redis.
// config/cache.php
'memory' => ['driver' => 'memory', 'key' => 771055301, 'size' => 320000],
// a queue worker increments...
Cache::store('memory')->increment('signups:today');
// ...and the next web request sees it. Exact under contention:
// 16 processes x 50 increments x 6 rounds == 4800 every time.Why not APCu? Laravel's built-in apc driver is the right answer when
only your web workers need the cache. APCu memory belongs to one process
family: the PHP-FPM pool shares it, but every CLI process (artisan,
queue:work, a cron job) gets its own private, empty APCu that dies with
the process. A counter bumped from a queue worker is invisible to the web
request. A SysV segment is keyed by number, not by process, so anything on
the host that opens the same key sees the same bytes.
What it is not for. The whole store is serialised into one fixed-size segment and rewritten on every write, so it is a poor fit for large or write-heavy caches. Read Limitations before using it in production, especially the overflow behaviour and how it discards locks.
- Requirements · Installation · Quick Start
- Configuration · How It Works · Atomicity · Locks · Stats
- Limitations · Testing Your Application
- Roadmap · Upgrading · Contributing
- Versioning · Authors · License
- 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.
composer require sanchescom/laravel-cache-memoryThe service provider (Sanchescom\Cache\MemoryServiceProvider) is
registered automatically via package auto-discovery — no manual
registration needed.
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');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).
- 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.
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.
The store implements Laravel's LockProvider, so Cache::lock() works on
it and follows the usual API:
$lock = Cache::store('memory')->lock('report:nightly', 120);
if ($lock->get()) {
try {
// exclusive across every process on the host
} finally {
$lock->release();
}
}
// block up to 5 seconds for the lock, then run and release automatically
Cache::store('memory')->lock('report:nightly', 120)->block(5, function () {
// ...
});
// release from another process (a queue job that finishes the work)
Cache::store('memory')->restoreLock('report:nightly', $owner)->release();A lock is an entry in the store under the lock's name, holding the owner
token. acquire() goes through the atomic add() (also when seconds is
0, unlike the framework's generic CacheLock), and release() checks
the owner and removes the entry in one critical section, so an expired lock
re-acquired by another process cannot be released by the old holder.
Mutual exclusion is covered by a forked test: 16 processes each perform 20
non-atomic read-modify-writes under the lock and the counter ends at
exactly 320.
Warning
Lock names share the key space with cache keys (as with every Laravel
cache-backed lock): Cache::put('job', …) and Cache::lock('job') use the
same entry. flush(), requestDeletion() and the overflow path discard
held locks together with the data. refresh() is not supported.
Cache::store('memory')->getStore()->stats();
// ['key' => 771055301, 'capacity' => 319996, 'used' => 1842, 'free' => 318154, 'items' => 12, 'expired' => 3]used is the serialised size of the whole store; expired entries are
counted, not purged. The same numbers as a table:
php artisan memory:stats # the "memory" store
php artisan memory:stats hot # any other memory-driver store
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
flush(), requestDeletion() and overflow discard locks too. Locks
are entries in the same segment as the data (see Locks); any
path that empties the store releases every held lock at once.
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.
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().
See ROADMAP.md for planned features (configurable permissions and an APCu fallback driver).
See UPGRADE.md for breaking-change notes, including the 1.x → 2.0 migration.
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
We use SemVer for versioning. For the versions available, see the tags on this repository.
- Efimov Aleksandr - Initial work - Sanchescom
See also the list of contributors who participated in this project.
This project is licensed under the MIT License - see the LICENSE.md file for details.
