Open-source project
laruence/yac avatar
laruence/yac

Yac: a shared memory cache inside the PHP process, with a 48 byte key limit

Fast, lock-free shared-memory user-data cache for PHP

835 stars199 forksCNOASSERTION

At a glance

What is it?
Yac is a C extension that keeps cache data in shared memory next to the PHP process, with no locks and no network round trip. It is built for a single node, it treats a corrupted entry as a miss, and since 2.4.0 get() can take a default value so a miss is distinguishable from a stored false.
Who is it for?
Yac fits a single node PHP application that caches small values such as flags, options and tokens across many worker processes, and that can accept a per-node cache rather than a shared one. It does not fit a multi node deployment, where the README itself points you at Memcached or Redis, and it is a poor fit for large objects given the value size limits.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The cache lives in the same process space as PHP

Yac is described as a lock-free shared-memory user data cache for PHP, and the first thing to understand is the topology. It lives in the same process space as PHP, with no network round trip, and it avoids coarse-grained locks. The consequence is that get() is essentially a hash lookup in shared memory, at microsecond level latency.

That is the whole design. There is no server to run, no socket to connect to and no protocol. In a PHP-FPM or similar setup where many worker processes serve requests from one machine, the workers share the same memory region and the same cache.

The repository shows the implementation split: yac.c is the extension, php_yac.h and the two arginfo headers carry the API surface, yac.php and yac.stub.php hold the PHP level class, and there are separate directories for storage/, serializer/ and compressor/. config.m4 and config.w32 are the build configurations for Unix and Windows, and package.xml is the PECL package definition.

It is also worth knowing what the project says this is not for. The README notes that for configuration, credentials, feature flags and routing tables, Yaconf beats Yac, and the hint is about durability and semantics rather than speed. Yac is a fast cache, not a configuration store.

Single node is the hard boundary, not an implementation detail

The README is unusually direct about where Yac belongs. It is best for single-node PHP apps chasing extreme cache performance, and the thing to watch out for is deployments that must share one cache across nodes, where the advice is to use Memcached or Redis.

That boundary follows from the design rather than being a policy decision. Shared memory is per machine. Two application nodes behind a load balancer each have their own Yac segment, so a value cached on one node is invisible to the other. Whether that matters depends entirely on your cache semantics. If a miss on one node simply causes a recompute, the cost is duplicated work rather than incorrect data, and Yac is fine. If the cache holds a counter, a rate limit, or a session, the per-node split changes behaviour, and that is the case the README is steering you away from.

The comparison with Memcached is instructive because the difference is architectural. Memcached is a separate process reached over a socket, which means a network round trip on every get and a shared view across nodes, and it is fast in absolute terms. Yac removes the round trip by moving the data into the process, and pays for it with a cache that is local to the machine.

There is a third option that the README mentions for the specific case of values that need to survive a restart, which is a local memcached, and it can replace APC or local memcached according to the README's own framing.

Three ways to install, and the CLI trap that catches everyone

Yac is a PHP extension, so it installs like one. The requirement is PHP 7 or newer, and the primary route is PECL:

bash
$ pecl install yac

PIE, the PHP Installer for Extensions, is the newer path and downloads, builds and installs from Packagist:

bash
$ pie install laruence/yac

And compiling from source is the usual three steps:

bash
$ /path/to/phpize
$ ./configure --enable-yac --with-php-config=/path/to-php-config
$ make && make install

The trap is the CLI mode. Yac is disabled in CLI mode by default, and the README says so in bold. If you are testing or running scripts from the command line, you have to add this to your php.ini:

ini
yac.enable_cli = 1

Otherwise new Yac() throws an exception. The reason for the default is straightforward: a CLI process is often a short-lived script, and a cache that persists in shared memory for a process that runs once and exits is mostly useless. But it does mean that the first thing a new user does, running a script from the terminal, fails with an exception that says nothing about the cause.

The rest of the INI settings follow the same pattern, and the defaults are worth reading carefully:

ini
yac.enable = 1

yac.keys_memory_size = 8M  ; 8M can hold ~64K key slots, scaling roughly linearly

yac.values_memory_size = 64M

yac.compress_threshold = 4K ; -1 disables compression. A positive value N means
                            ; values larger than N bytes will be compressed before storage.
                            ; Values below 1024 are clamped to 1024,
                            ; values above the 1M stored-entry limit are clamped to it.

yac.enable_cli = 0  ; whether to enable Yac in CLI mode, default 0

The keys and values have separate memory budgets, which is the right split: keys are hashed and indexed, values are payloads, and most workloads have many more keys than large values.

Three hard limits, and a key longer than 48 bytes has to be hashed

The restrictions section lists three limits, and they are not advisory.

A cache key cannot be longer than 48 bytes, which is the YAC_MAX_KEY_LEN constant. A cache value cannot be longer than 67108863 bytes, which is YAC_MAX_VALUE_RAW_LEN, that is (1 << 26) - 1. A cache value after compression cannot be longer than 1M, which is YAC_MAX_RAW_COMPRESSED_LEN.

The key limit is the one that bites, because the constants section shows what to do about it: if your key is longer than 48 bytes, consider using md5() of the key instead. A 32 character hex digest fits comfortably, so the workaround is straightforward, but it is not free. You now hash on every set and every get, and the original key is no longer visible in a memory dump or a debug view. For a cache that claims to be about speed, adding a hash to every operation is a real cost, and it is the cost of using long composite keys.

The compression limit interacts with the threshold setting in a way that is easy to misread. The threshold means values larger than N bytes get compressed before storage, and the documented clamps are that values below 1024 are clamped up to 1024 and values above the 1M stored-entry limit are clamped down to it. So a threshold of 4K does not mean that only values over 4K are compressed and everything else is untouched, it means the effective threshold is somewhere between 1K and 1M depending on what you set.

The values memory size of 64M is a hard ceiling on what you can hold, and it is separate from the per value limits.

get() cannot tell a miss from a stored false, unless you give it a sentinel

The supported data types table is followed by the subtlest part of the API, and it is worth reading twice.

Yac can store all PHP types except resources. null is stored as is and comes back as NULL, while a miss returns false. bool is stored as is. int, long, float and double are stored directly. string is stored directly and compressed if larger than the threshold. array uses Yac's internal fast serializer when the values are supported, and PHP serialization otherwise. stdClass uses the internal fast serializer when the properties are supported and is restored as stdClass. Other objects go through PHP serialization. resource is not supported and triggers a warning.

Then there is the missing key problem. Without a second argument, get() returns false for a key that does not exist, or that fails the integrity check, which is the same value a stored false returns, so the two cases are indistinguishable by return value alone.

Since 2.4.0, get() accepts an optional default argument that is returned when the key is missing:

php
<?php
$yac->set("f", false);

var_dump($yac->get("f"));                    // bool(false) — the stored value
var_dump($yac->get("missing"));              // bool(false) — a miss, same shape

var_dump($yac->get("f", "__NONE__"));        // bool(false) — the stored value
var_dump($yac->get("missing", "__NONE__"));  // string(8) "__NONE__" — the default
?>

With a sentinel default, a miss can be told apart from every stored value, including a stored false and a stored NULL. The default is passed by value, so any expression works, such as get("n", 0) or get("n", []). That is a small API addition with a large effect on how safely you can write the calling code, and it is the difference between a cache that can store booleans and one that cannot be used to store booleans without a wrapper.

Every value carries a CRC-32C, so corruption becomes a miss

The note section explains the integrity behaviour, and it is a design choice with a clear trade-off. Yac checks every stored value with a full CRC-32C, so a corrupted or overwritten entry is detected and reported as a miss rather than returned.

That is the safe direction to fail in. Returning garbage from a cache that has been overwritten by another process, or that has been damaged, would propagate bad data through your application. Reporting a miss means the application recomputes and carries on. The cost is a checksum computation on every read and every write, in a project whose entire premise is speed.

The second note concerns debugging. When Yac is built with a PHP debug build, it enables concurrency instrumentation and exposes the YAC_DEBUG constant. The README is explicit that this cannot be enabled at runtime, which is the correct constraint: instrumentation that changes the timing of the thing it measures cannot be switched on while the measurement is running.

So the debugging story requires a separate build of PHP. That is a real cost for a library whose main virtue is that it is easy to drop into an existing stack, and it is worth knowing before you go looking for a switch that does not exist.

The published benchmark measures throughput, and says so

The README leads with a benchmark table comparing Yac against APCu 5.1.28 and Memcached 3.4.0, and the conditions are stated: 16 worker processes sharing one cache, mixed reads and writes at a 100 to 1 ratio, 160M of shared memory for both Yac and APCu, with numbers as aggregate ops per second across all workers and one value size per run.

The results are large multiples. At 6 bytes, which the README explains represents the class of small values PHP caches actually hold, feature flags, booleans, integers and short strings, the table gives Yac at 129.5M ops per second against 1.19M for APCu and 0.105M for Memcached, a ratio of 108.4x and 1237.9x. At 64 bytes it is 99.1M against 1.23M and 0.103M. At 1 KB, 39.4M against 1.29M and 0.106M. At 8 KB, 15.5M against 1.43M and 0.098M. At 16 KB, 9.51M against 1.36M and 0.093M. The README states that all backends hit 100% at every listed size.

Two caveats are printed next to the table and they matter more than the ratios. The numbers are aggregate throughput, not consistency, and the build is from the current master branch. The 6 byte case is also explained rather than presented as a trick: the size is chosen because it packs into Yac's pointer word, which is its fastest storage path.

Read the ratios as a statement about the cost of a network round trip rather than as a portable number. The bench/README.md file holds the environment and the reproduction steps, and if you are making a decision, running that against your own key and value distribution is the only measurement that counts.

Where Yac is the wrong tool

The first wrong fit is a multi node deployment, and this is the one the README itself calls out. If the cache must be shared across nodes, Yac cannot be it, because shared memory ends at the machine boundary. Memcached or Redis is the answer, and the difference in approach is exactly the thing you are trading away: a network round trip and a shared view, in exchange for locality.

The second is large values. With a per value limit of one megabyte after compression and a 64M total values budget, Yac is a cache for small things. A megabyte of compressed payload also means the compression itself is on the hot path for anything above the threshold, which is a trade the benchmark's 6 byte case never exercises.

The third is long keys. The 48 byte limit means a natural key like a URL with a query string, or a composite of several identifiers, has to be md5 hashed, and you pay a hash on every operation to work around a design constraint. A key with a natural short form is a precondition for getting the speed the project claims, even though the README does not put it that way.

The fourth is needing durability. Nothing in the shared memory segment survives a machine restart, and nothing is shared with a second process on another host. If the cached value is expensive to recompute and you cannot afford to recompute it after a deploy, a persistent store is the right tool, and the README's own pointer to Yaconf for configuration is an example of that instinct.

The fifth is a team that wants a shared cache and is on Windows or a platform where the extension is not a first class citizen. The repository carries a config.w32 build file and an AppVeyor badge, so it does build there, but the ecosystem around a PECL extension on Windows is thinner than on Unix, and the deployment story is not the same.

Editorial conclusion

Yac fits a single node PHP application that caches small values such as flags, options and tokens across many worker processes, and that can accept a per-node cache rather than a shared one. It does not fit a multi node deployment, where the README itself points you at Memcached or Redis, and it is a poor fit for large objects given the value size limits. Before adopting it, measure your own key size distribution, because a key over 48 bytes has to be hashed and that changes both the cost and the failure mode.

Frequently asked questions

What is Yac and when should I use it?

Yac is a lock-free shared-memory user data cache for PHP, written in C, that lives in the same process space as PHP with no network round trip. The README says it is best for single-node PHP apps chasing extreme cache performance, and that deployments needing one cache across nodes should use Memcached or Redis.

How do I install the Yac PHP extension?

With PECL using pecl install yac, with PIE using pie install laruence/yac, or from source with phpize, ./configure --enable-yac and make && make install. PHP 7 or newer is required.

Why does new Yac() throw an exception in my script?

Yac is disabled in CLI mode by default. Add yac.enable_cli = 1 to your php.ini, and the extension will work from the command line.

How do I tell a cache miss from a stored false value in Yac?

Without a second argument, get() returns false for both. Since 2.4.0 you can pass a default value, so a miss returns the default while a stored false still returns false, for example $yac->get("f", "__NONE__").

What are the size limits for Yac cache entries?

A key cannot exceed 48 bytes, which is YAC_MAX_KEY_LEN, and a value cannot exceed 67108863 bytes raw or 1M after compression. The README suggests using md5() of a key when it is longer than 48 bytes.

Official sources

  1. Issues
  2. laruence/yac on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/laruence-yac.svg)](https://hysenlabs.com/projects/laruence-yac)

Was this useful?