In PHP, when does caching a feature-flag list in APCu on each web server beat Redis, and when does it give wrong answers?
answer
- no network round-trip, no serialization over the wire
- one copy per server
- no cross-server invalidation
- staleness bounded by the TTL
- CLI and restarts see different caches
basics
~20 sAPCu wins for small, read-heavy, rarely changing data such as a flag list: a local memory read, no network hop. It gives wrong answers when servers must agree, because each server holds its own copy and cannot invalidate the others.
solid answer
~50 sAPCu reads from the server's own shared memory, so a lookup costs a memory copy rather than a network round-trip and survives an outage of the cache server. For a feature-flag list read on every request and changed a few times a day, that is ideal: `apcu_store('flags:v1', $flags, 30)` on each server, refreshed from the source of truth when it expires. The price is **coordination**. Each server has its own copy, so after a flag change servers disagree for up to the TTL; `apcu_delete()` on one server, or from a deploy script run with the CLI, clears nothing elsewhere. A PHP-FPM restart empties one server's store, and a new server starts cold. Use a shared cache such as Redis when every node must see the same value at once, when data is written by requests, or when the data set is too large to copy onto every server.
go deeper
Remember that APCu lives on one server and a network cache is shared. Local is faster; shared keeps every server on the same value.
Explain the per-server copy, bounded staleness by TTL, and why a CLI script or one server's delete does not clear the other caches.
Pick APCu or a shared cache from the data's properties: size, write rate, staleness tolerance, rebuild cost. Plan cold starts and deploy-time invalidation explicitly.
Frame the choice as a consistency budget: which data may be stale per server and for how long, and where a two-tier design with a shared source of truth is worth its complexity.
## Two kinds of cache A PHP application can cache data in two very different places: - A **local, in-process-tree cache** such as APCu: each server keeps its own copy in shared memory, and PHP-FPM workers on that server read it directly. - A **shared network cache** such as Redis or Memcached: one logical store that every server reaches over the network. The choice is a trade between speed and independence on one side, and coordination on the other. ## The feature-flag scenario A shop reads a list of about 40 feature flags on every request. The flags live in a database table and change a few times a day. Reading the table on every request is wasteful; caching it is obvious. The question is where. ```php <?php declare(strict_types=1); function flags(PDO $pdo): array { $flags = apcu_fetch('shop:flags:v1', $hit); if (!$hit) { $flags = $pdo->query('SELECT name, enabled FROM feature_flags') ->fetchAll(PDO::FETCH_KEY_PAIR); apcu_store('shop:flags:v1', $flags, 30); } return $flags; } ``` With APCu, each web server fetches the table at most about once every 30 seconds, and every request in between reads a local copy. ## Where APCu wins | Property | APCu | Network cache | |---|---|---| | Cost per read | a shared-memory lookup and copy | a network round-trip plus deserialization | | Failure dependency | none beyond the server itself | the cache server and the network | | Data size that fits | small; copied onto every server | large; stored once | | Consistency across servers | none; each server has its own copy | one value for everyone | | Invalidation | local only | one delete is seen by all | | Survives a PHP-FPM restart | no | yes | APCu is the better fit when the data is: 1. **small** enough to copy onto every server; 2. **read far more often than written**; 3. **tolerant of bounded staleness**, where a few seconds of disagreement between servers is acceptable; 4. **cheap to rebuild** when a server starts cold. A feature-flag list, a country list, a routing table or a compiled configuration array usually meets all four. ## Where APCu gives wrong answers - **Servers disagree after a change.** Turn a flag off at 12:00:00 and each server keeps serving its cached copy until its own entry expires. With a 30-second TTL, for up to 30 seconds some requests see the old value and some the new, depending on which server handles them. A user can see a feature appear and disappear between two clicks. - **Invalidation does not propagate.** An admin action that calls `apcu_delete('shop:flags:v1')` clears the cache on the one server that handled that request. The others keep the stale entry until it expires. - **The CLI has a different cache.** A deploy or cron script run with `php` starts its own process, and APCu is disabled there by default (`apc.enable_cli=0`). Even when enabled, a CLI process gets its own short-lived store, so `apcu_clear_cache()` from a deploy script does not touch what PHP-FPM workers read. - **Restarts and new servers start cold.** A restart or a scale-out produces a burst of rebuilds against the database, one per server. - **Writes from requests do not belong here.** Counters, sessions, rate limits or carts updated by requests would diverge per server; they need one shared store. ## Making APCu's weaknesses acceptable - Pick the TTL as the **maximum staleness** you accept, not as a performance knob. - For urgent changes, version the key (`shop:flags:v2`) in a deploy so every server misses immediately after the new code arrives. - To clear APCu on purpose, do it through a web request to each server, or by restarting PHP-FPM, not from a CLI script. - If flags must switch everywhere at once (a kill switch for a broken payment provider), read that particular flag from a shared store. Whatever you choose, write down the maximum staleness the product accepts for each cached value; the TTL follows from it, and the choice of APCu or a shared cache follows from whether that staleness may differ between servers. Some teams combine both: a shared cache as the source every server agrees on, and APCu with a short TTL in front of it to absorb the per-request reads.
- A deploy script runs php bin/clear-cache.php, which calls apcu_clear_cache(), but web requests keep serving old data. Why?The script runs in a separate CLI process. APCu is disabled in the CLI by default (`apc.enable_cli=0`), and even when enabled the CLI process gets its own short-lived store, not the shared memory of the PHP-FPM master. Clear the web cache from inside PHP-FPM, for example through an internal endpoint on each server, or by restarting PHP-FPM, or change the key prefix in the deploy.
- How would you make a flag change visible on all servers within one second without giving up APCu?Keep APCu only as a short buffer: a TTL of about one second in front of a shared store such as Redis or the database. Each server then consults the shared source at most once a second, which is still far fewer reads than one per request, and servers never disagree for longer than that TTL. The shared store carries the truth.
APCu is a printed timetable pinned up in each station: reading it is instant and works when the head office phone is down, but when the schedule changes, each station keeps its old sheet until its next scheduled reprint. A network cache is phoning head office every time: slower, but everyone hears the same answer.
saying these in an interview costs you the question
- Assumes apcu_delete() on one web server clears that key on all servers.
- Clears the web servers' APCu from a CLI deploy script and expects it to work.
- Uses APCu for counters or rate limits that must be global across servers.
- Picks the APCu TTL for speed without considering how long servers may disagree.
- Believes APCu entries survive a PHP-FPM restart like Redis data does.