skip to content

In PHP, how do APCu's apcu_store() and apcu_fetch() keep data between requests, and why is comparing apcu_fetch()'s result with false not enough?

level: juniorimportance: should knowfreq 36%

answer

  1. shared memory of one server
  2. key, value, optional TTL in seconds
  3. a copy in, a copy out
  4. false on a miss
  5. the by-reference $success argument

basics

~20 s

APCu is a PECL extension that keeps user data in the shared memory of one PHP server, readable by later requests. apcu_fetch() returns false on a miss, so a cached false is indistinguishable unless you pass its by-reference $success argument.

solid answer

~40 s

PHP normally forgets everything at the end of a request. APCu keeps a key-value store in **shared memory** that every worker process of the same PHP-FPM master (or web-server parent) can read. `apcu_store(string $key, mixed $var, int $ttl = 0)` copies a value in and returns `true` on success; a `$ttl` of `0` means no expiry. `apcu_fetch(mixed $key, bool &$success = null)` copies the value back into the request's memory, or returns `false` on a miss. That is the trap: a stored `false` (or a failed lookup that you then cache) looks exactly like a miss. Pass the second argument, `apcu_fetch($key, $hit)`, and check `$hit`. Every fetch returns a fresh copy: objects go through a serializer, so large objects cost CPU on each read, and changing the fetched value never changes the cached one.

code

php · 16 lines
php
<?php
declare(strict_types=1);

function countries(PDO $pdo): array
{
    $countries = apcu_fetch('shop:v3:countries', $hit);
    if ($hit) {
        return $countries;
    }

    $countries = $pdo->query('SELECT code, name FROM countries ORDER BY name')
        ->fetchAll(PDO::FETCH_KEY_PAIR);
    apcu_store('shop:v3:countries', $countries, 3600);

    return $countries;
}

go deeper

for a junior

Know that APCu keeps data between requests on one server, and the three calls: apcu_store with an optional TTL, apcu_fetch, apcu_delete.

for a middle

Explain the $success argument and why false is ambiguous, the copy semantics of fetch, and the serializer cost for objects. Mention key prefixes when several applications share a server.

for a senior

Discuss what the cache can lose and when: restarts, evictions, no sharing across servers. Show that every read path can rebuild the value and that entry size is chosen with the per-fetch copy in mind.

for a principal

Weigh which data belongs in a per-server memory cache at all, given its lack of coordination across servers, versus a shared cache that every node sees.

## Why APCu exists A PHP web request starts with empty memory and throws everything away when it finishes. That share-nothing model is simple and robust, but it means expensive results, such as a parsed configuration file, a list of countries from the database, or a computed permission map, are rebuilt on every request. **APCu** (the user-cache half of the old APC extension, distributed through PECL) adds a key-value store that lives in the **shared memory** of the PHP server process tree. Every PHP-FPM worker forked from the same master reads and writes the same store, so a value stored by one request is visible to the next request, whichever worker handles it. ## The core functions | Function | What it does | Returns | |---|---|---| | `apcu_store(string $key, mixed $var, int $ttl = 0)` | stores or overwrites a value | `true` on success, `false` on failure | | `apcu_add(string $key, mixed $var, int $ttl = 0)` | stores only if the key does not exist | `false` if the key is already there | | `apcu_fetch(mixed $key, bool &$success = null)` | reads a value, or an array of values for an array of keys | the value, or `false` on a miss | | `apcu_delete(mixed $key)` | removes one key, several keys, or those matched by an `APCUIterator` | `true` or `false` | | `apcu_exists(string\|array $keys)` | checks presence without copying the value | `bool`, or an array for several keys | | `apcu_enabled()` | tells whether APCu is usable in this environment | `bool` | `$ttl` is in seconds. `0` means the entry never expires on its own; it lives until it is deleted, evicted, or the cache is cleared or restarted. ## The false-on-miss trap `apcu_fetch()` returns `false` when the key is missing or expired. If `false` is also a legitimate cached value ("this user has no discount", "the feature is off"), a naive check turns every hit into a miss: ```php <?php declare(strict_types=1); $enabled = apcu_fetch('flag:new-checkout', $hit); if (!$hit) { $enabled = loadFlagFromDatabase('new-checkout'); // may be false apcu_store('flag:new-checkout', $enabled, 30); } ``` The second, by-reference argument `$success` is set to `true` on a hit and `false` on a miss, independently of the value. The same applies to `null`, `0` and empty arrays if your code uses loose checks. ## Copies, not shared objects APCu does not hand out references into shared memory: - `apcu_store()` **copies** the value into shared memory. Objects, and arrays containing objects, pass through the serializer named by `apc.serializer` (default `php`). - `apcu_fetch()` **copies** it back into the request's own memory, unserializing where needed. - Changing the fetched array or object changes only the local copy. To update the cache you must store again. Consequences: 1. Fetching a 5 MB array costs a 5 MB copy on every request that needs it. Store what you actually read, not everything you have. 2. Objects cost serialization on store and unserialization on every fetch. Plain arrays and scalars are cheaper. 3. Resources such as database connections and file handles cannot be meaningfully cached; they belong to one process. ## What APCu is not - It is **not OPcache**. OPcache stores compiled scripts; APCu stores data your code chooses to put there. - It is **not shared between servers**. Each machine, or each container with its own PHP-FPM master, has its own store. - It is **not durable**. A PHP-FPM restart empties it, and a full cache evicts entries. Treat it as a cache that can lose anything at any time, and always have a way to rebuild the value. - It is **off in the CLI by default** (`apc.enable_cli=0`), so scripts and tests run from the command line do not see it unless you enable it. ## Namespacing keys Because every application on the same PHP-FPM master shares one store, prefix keys with the application and a version (`shop:v3:countries`). Bumping the version in a deploy makes old entries unreachable at once, and they age out or are evicted later. Choose key names that say what the value is and which version of the code produced it, so a stale entry from an older release can never be read by newer code expecting a different shape.

  • You fetch an array from APCu, append an element to it, and read it again with apcu_fetch() in the next request. Is the element there?
    No. `apcu_fetch()` returns a copy in the request's own memory, so appending changes only that copy. The shared-memory entry is untouched until you call `apcu_store()` again with the new array. This is also why concurrent requests cannot corrupt each other's view of an entry: they each work on private copies.
  • Why is caching a large object graph in APCu sometimes slower than rebuilding it?
    Objects are serialized on `apcu_store()` and unserialized on every `apcu_fetch()`, and the whole value is copied each time. For a large graph that is read partially, the copy and unserialize work can cost more than the query or computation it replaced. Cache smaller, plain arrays keyed by what requests actually read, and measure both paths.

saying these in an interview costs you the question

  • Checks apcu_fetch() with === false and caches boolean flags in the same store.
  • Believes apcu_fetch() returns a reference that updates the cache when modified.
  • Thinks an APCu entry stored on one web server is visible on the others.
  • Confuses APCu with OPcache and expects it to cache compiled PHP scripts.
  • Treats an APCu entry with TTL 0 as permanent storage that is never lost.