Search by

chrfritsch / nocopy

fritze

Zero-copy shared-memory cache for PHP

Package info

github.com/chrfritsch/nocopy

Type:php-ext

Ext name:ext-nocopy

pkg:composer/chrfritsch/nocopy

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.0.1 2026-11-08 08:51 UTC

This package is auto-updated.

Last update: 2026-11-08 17:16:18 UTC


README

nocopy

A cache for PHP that reads a 1,000-row array as fast as a single integer.

Simple put / get functions, one cache shared by every PHP worker on the machine, and no copying on read.

License: MIT PHP 8.4 | 8.5 NTS and ZTS Linux amd64 | arm64

nocopy_put('products', $rows);      // 1,000 rows
$rows = nocopy_get('products');     // APCu: ~200 µs. nocopy: ~12 ns. Any size.

Status: 0.0.1, the first release: works and is tested, no stability promise yet. Install with pie install chrfritsch/nocopy (status and roadmap).

The idea in one picture

APCu and similar caches keep your data in memory that all PHP workers share. But every time you read a value, they copy it into your request, element by element. A big array means a big copy, on every single read.

nocopy hands your code the shared value itself. Nothing is copied, so reading a huge array costs the same as reading a number.

flowchart LR
    subgraph APCu
        A1[(shared memory)] -- "copy every element<br/>~200 µs" --> A2[your request]
    end
    subgraph nocopy
        N1[(shared memory)] -- "hand over a pointer<br/>~12 ns" --> N2[your request]
    end
Loading

Think of a library: APCu photocopies the whole book each time you want to read it. nocopy lets you read it right on the shelf. And if you write in it? PHP quietly makes you a private copy first, exactly as it does with any array you pass around. Your code never notices.

How much faster?

without nocopy with nocopy
Read an array with 1,000 rows (vs APCu) 200 µs 12 ns
Read an int / a 100-byte string (vs APCu) 28.2 / 34.4 ns 11.0 / 11.5 ns
Reads per second (int), 8 workers in parallel (vs APCu) 3.7 M 350 M
Write an int / an array with 1,000 rows (vs APCu) 55.3 ns / 191 µs 27.3 ns / 59 µs
Writes per second (int), 8 workers in parallel (vs APCu) 10.4 M 141 M
Symfony app, cache-heavy page (vs APCu) 4,542 req/s (25% of an empty page) 17,430 req/s (~100%)
Drupal 11, one server (vs stock): anonymous / logged-in node page 4,671 / 835 req/s 12,145 / 1,122 req/s
Drupal 11, several servers (vs drupal/redis): anonymous / logged-in node page 4,837 / 634 req/s 12,538 / 1,152 req/s
Drupal 11, JSON:API list of 20 articles, logged in (vs stock) 1,200 req/s 2,084 req/s
Drupal 11, database queries per logged-in request (vs stock) 18–31 7
Symfony cache-heavy pages: nocopy 17,430 and 15,801 req/s, about all of an empty page (~17,000); APCu 4,542 and 4,416 req/s, 25% and 24%

Linux, Docker on arm64 (Apple M3 Pro), PHP 8.5; the read ratios hold on AWS Graviton4 (writes not cross-checked yet). How to reproduce: benchmark results.

The fine print: the no-copy trick pays off when your requests read big cached arrays (config, routes, translations, entity lists). If your pages barely touch the cache, you won't see a difference. Objects are rebuilt from their stored properties without unserializing (except Serializable and most internal classes: serialized, as in APCu).

On Drupal, most of the gain comes from something else: because php-fpm, Drush and cron share one cache that survives restarts, every cache bin and cache tag can move out of the database. Not copying adds little there (breakdown).

What you get

⚡ Fast reads No copy, no unserialize, no lock while reading.
🖥️ One cache for web and CLI php-fpm, Drush, bin/console and cron jobs all see the same cache, and it survives php-fpm restarts.
🧹 Graceful when full APCu throws away the whole cache when memory runs out. nocopy removes the oldest, least-read entries and keeps the popular ones.
🚦 No stampedes nocopy_remember() computes an expensive value once, even if 100 workers ask for it at the same moment, and refreshes it shortly before it expires.
🌐 Several servers Point it at Valkey (the open-source Redis fork) and all servers share one cache, while each keeps a fast local copy that stays in sync automatically. No extra PHP extension needed.
🛡️ Crash-safe A worker killed mid-write (OOM killer, kill -9) can't corrupt the cache: the damage is detected and repaired.
🧩 Framework integrations Symfony bundle (cache.adapter.nocopy), Drupal module (all cache bins and cache tags).
🔁 Easy switch from APCu A polyfill provides the apcu_* functions on top of nocopy, for code you can't change yet (differences).
🔍 Console nocopy-cli shows stats and keys, reads, deletes and clears entries in the running cache, like cachetool.

Works with php-fpm, Apache, LiteSpeed, CGI, php -S and FrankenPHP. Long-running processes (CLI scripts, queue workers, FrankenPHP worker mode) automatically get a safe mode that hands out private copies and reuses them while the entry is unchanged.

Try it in one minute

All you need is Docker:

git clone https://github.com/chrfritsch/nocopy && cd nocopy
./dev shell
php -r 'nocopy_put("a", [1, 2, 3]); var_dump(nocopy_get("a"));'

Use it

The whole API fits on a sticky note:

nocopy_put('products', $rows, ttl: 300);    // store for 5 minutes
$rows = nocopy_get('products', $hit);       // $hit is true if it was found

$report = nocopy_remember('report', fn () => buildReport(), ttl: 60);   // compute once, cache, return

nocopy_inc('page_views');                   // atomic counter
nocopy_delete('products');

Also: nocopy_add, nocopy_cas, nocopy_get_multiple, nocopy_put_multiple, nocopy_delete_multiple, nocopy_clear, nocopy_keys, nocopy_stats, nocopy_shared_with. All of them: function reference.

Install on a server

You need Linux, PHP 8.4+ with its dev tools (phpize) and a C compiler. Install it with PIE:

pie install chrfritsch/nocopy

Then configure it in a file read by both php-fpm and the CLI (e.g. conf.d/nocopy.ini):

extension=nocopy            ; PIE adds this line itself
nocopy.shm_name=/nocopy     ; required: the cache's name, so web and CLI share it
nocopy.shm_size=128M        ; how much memory the cache may use

Step by step (Docker, Valkey over TLS, upgrades): install guide. Several servers: Valkey guide.

Integrations

The Composer packages are not on Packagist yet. Require them from a checkout of this repository:

composer config repositories.nocopy-apcu path /path/to/nocopy/apcu
composer require chrfritsch/nocopy-apcu:@dev          # apcu_* polyfill

Same for symfony/ (chrfritsch/nocopy-bundle), drupal/ (chrfritsch/nocopy-drupal, needs the polyfill too) and cli/ (chrfritsch/nocopy-cli). Setup: Symfony, Drupal, APCu polyfill.

Learn more

New here Using it How it works
Your first cache (10-minute tutorial) Symfony · Drupal · Migrate from APCu How nocopy works
Install on a server Share between php-fpm and CLI · Valkey Design decisions (with numbers)
Size and monitor · Configuration · nocopy_stats() Architecture (internals)

Everything: docs/.

Contributing

Bug reports and pull requests are welcome. For a bug, please include your PHP version, how PHP runs (php-fpm, FrankenPHP, CLI…), whether it's thread-safe (php -v shows ZTS or NTS), and the output of nocopy_stats().

You don't need PHP or a compiler on your machine, just Docker: ./dev test builds and runs the tests, ./dev ci runs what CI runs. Start with the first change tutorial, then Develop, test and benchmark.

License

MIT © 2026 Christian Fritsch. Eviction design inspired by Segcache (Yang et al., NSDI 2021); the polyfill follows APCu's API.