High-performance PSR-16 and PSR-6 in-memory cache backed by Judy arrays, built for long-running PHP: Laravel Octane, Swoole, RoadRunner, FrankenPHP worker mode, queue workers, and CLI daemons — anywhere process memory survives across requests. (In classic FPM the process dies with the request; use APCu there.)
- O(range) Prefix Invalidation: Invalidate key hierarchies instantly without scanning the whole cache.
- Up to 70% Less Memory: Radix-trie storage compresses key/bucket overhead at scale (172 MB vs 495 MB at 1M keys).
- Works Out of the Box: Uses orieg/judy-polyfill by default; automatically upgrades to native C ext-judy >= 2.6.0 when loaded.
composer require orieg/judy-cache
# Optional for native C performance:
pie install orieg/judyNote: If you install the C extension, use ext-judy >= 2.6.0 for memory-safe teardown on mixed-type arrays (php-judy#162).
use Orieg\JudyCache\JudySimpleCache;
$cache = new JudySimpleCache();
$cache->set('user.42.profile', $profile, ttl: 300);
$profile = $cache->get('user.42.profile');Because the trie maintains keys in lexicographic order, range operations walk only matching keys (
$cache->deletePrefix('user.42.'); // O(matching keys), not O(all keys)
$cache->keysByPrefix('report.', 100); // ordered prefix exploration
$cache->prune(); // eager eviction of expired entriesArray-backed caches (plain arrays, Symfony ArrayAdapter, APCu) have no fast path for range eviction and must scan every key.
With symfony/cache installed:
use Orieg\JudyCache\JudyAdapter;
use Orieg\JudyCache\JudySimpleCache;
$judy = new JudySimpleCache();
$pool = new JudyAdapter(cache: $judy); // PSR-6 cache pool
$report = $pool->get('report.7', fn () => computeReport(7));
$judy->deletePrefix('report.'); // range invalidation underneathExplore runnable implementations in examples/:
-
Large-Value Storage Shootout: Standalone headless CLI benchmark evaluating transparent adaptive compression, content-addressable interning, zero-alloc cursor pruning, and multi-worker memory models (
$W \times \text{Size}$ ). -
Live Multi-Worker Simulation: Multi-process simulation harness spawning real OS child worker processes to measure true host
VmRSSresident memory amplification across$W \in {1 \dots 64}$ workers. -
FrankenPHP Worker Mode Testbed: Interactive web dashboard with real-time SSE telemetry, live process VmRSS metrics, CRC lossless integrity verification, and side-by-side shootouts (
$10\text{k} \dots 10\text{M}$ keys). - Multi-Worker Owner Process: Reference implementation of an IPC/Unix-socket cache daemon providing a single-writer cache across multi-worker pools.
- Keys: Standard PSR-16 rules (
{}()/\@:reserved). Use.as your hierarchy separator (user.42.profile). - Values: Serialized snapshots by default (like Symfony's
ArrayAdapter), ensuring fetched objects are safe from mutation. PassstoreSerialized: falsefor faster by-reference storage. - TTL & Pruning:
intseconds orDateInterval. Expired entries are evicted lazily on access, or eagerly via native in-CpruneExpired()(or zero-allocation cursorprune()when external slab/shmop allocations exist). TheSTRING_TO_ENTRYC struct packs expiry timestamps and 16-bit metadata flags directly into the entry, eliminating secondary lookup arrays and userland packing overhead. - Adaptive Compression: Pass
compressionThreshold: 1024(bytes) andcompressionCodec: 'gzip'('gzip','deflate','zstd','lz4') to auto-compress payloads exceeding the threshold. Codec metadata is stored directly in 16-bit entry flags and automatically skips compression if the compressed size exceeds original payload size. - Content-Addressable Interning: Pass
enableInterning: true(and optionalinternThreshold: 256) to deduplicate identical payloads across distinct keys into a shared 8-byte binary XXH3 reference-counted pool. - Chunked Slab Arena: Pass
slabArena: new SlabArena()andslabThreshold: 1024to route large byte payloads (JSON docs, HTML fragments) into pre-allocated contiguous memory blocks via 8-byte uint64 chunk offsets to prevent Zend Memory Manager (ZMM) heap fragmentation. - Shared Memory Pool (shmop): Pass
shmPool: new SharedMemoryPool()andshmThreshold: 1024for zero-copy shared memory payload segments across multi-worker pools (FrankenPHP, Octane, Swoole). - Clock: Injectable (
new JudySimpleCache(clock: fn() => $timestamp)) for deterministic testing. - Backend Choice: Built natively on
Judy::STRING_TO_ENTRYfor optimal C-level TTL and metadata handling.
Measured on CI (PHP 8.4, ext-judy 2.6.0, median of 5, small serialized array values):
| Metric / Workload | judy-cache (ext-judy) |
Native PHP Array | Symfony ArrayAdapter |
Symfony TagAwareAdapter |
|---|---|---|---|---|
| RAM (1M entries) | 172 MB | 495 MB | 921 MB | 1,407 MB |
| Prefix Prune (50k) | 50 µs | 12.4 ms | 13.1 ms | 14.0 µs (deferred) |
| Prefix Prune (1M) | 58 µs | 252.0 ms | 268.0 ms | 14.0 µs (deferred) |
| Write Ops/s (set) | ~1.6M ops/s | ~830k ops/s | ~790k ops/s | ~85k ops/s (10x slower) |
Process Scope: Like PHP arrays,
judy-cacheis per-process. For data shared read-hot across many worker processes without invalidation needs, APCu remains optimal. For per-worker state, single-process daemons, and$O(\text{range})$ invalidation,judy-cacheprovides unmatched memory density and eviction speed.
See BENCHMARK.md for full benchmarks, methodology, and vendoring analyses.
Four independent layers executed across PHP 8.1–8.5 on both ext-judy and judy-polyfill:
- Spec Compliance:
tests/simplecache.phpverifies every testable clause of PSR-16. - Behavior Tests: Clock/TTL edge cases, prefix operations, and serialization semantics.
- Model-Based Fuzzing:
tests/fuzz.phptests randomized operations diffed step-by-step against an oracle. - Backend Parity: Fully verified against
php-judyC extension viajudy-polyfill's differential test suite.
MIT.