14 October 2026
📌 Past midnight in Gourock, Mandylion by The Gathering in the headphones (Anneke, what a voice), and a plugin list open in another tab. Five plugins left on the Bottega's WordPress, down from twenty-four a year ago. One of them is Redis Object Cache, and tonight's question was the lazy one: does it still do anything?
asking the numbers
The honest answer is that I didn't know. The Redis container has been in the compose file since the migration, the plugin has been active since day one, and nobody had ever checked whether the two were actually talking to each other.
They were. The drop-in was valid, the ping came back, zero errors. Then I asked Redis itself what it had been doing for the last nineteen days:
keyspace_hits: 99,865,187
keyspace_misses: 57,836,246
evicted_keys: 56,094,048
used_memory: 253.83M / 256M
Three things in there. A 63% hit ratio, which is mediocre for a WordPress object cache - a site with human traffic usually sits well above 80%. Memory pinned to the ceiling. And the number that explains the other two: almost every miss matches an eviction. Three million keys a day go in and get thrown out again.
That is not the cache failing. That is the scrapers walking the archive, one page per fresh IP, each article a key nobody will ever read twice. The policy is allkeys-lru, so the keys real readers touch - options, users, the front page, this week's posts - stay in. What gets evicted is the long tail of a 25-year archive being crawled by machines. A hundred million hits in nineteen days is a hundred million queries that never reached MariaDB, and that part works.
I thought about raising the memory limit and decided against it. More room would cache more bot pages, not more reader pages: the readers already fit. The whole archive does not fit in any Redis a small VPS can afford, and nobody is asking it to.
the C client
What the status page did show was Client: Predis. Predis is the Redis client written in PHP; PhpRedis is the same thing as a C extension, and the plugin prefers it when it finds it. The official wordpress:php8.3-fpm image doesn't ship it, so the site had been running on the fallback since September.
Fixing that means owning an image. Three lines:
FROM wordpress:php8.3-fpm
RUN pecl install redis \
&& docker-php-ext-enable redis \
&& rm -rf /tmp/pear
In the compose file the wordpress service gets a build: next to its image:, so a future PHP update is docker compose build --pull wordpress instead of a plain pull. That is the real cost of tonight: not the three lines, but the fact that from now on the web server runs a derived image instead of the one on the hub. One more thing that can break at patch time. Small, but mine.
the 502 I should have expected
Snapshot first, as always on this machine. Then up -d wordpress, container recreated, fpm is running in the log, and the site answered with a bad gateway.
Nginx resolves the PHP container's name once, when it reads its configuration, and keeps the address. Recreate the container, Docker may hand it a new one, nginx keeps knocking on the old door. The nginx error log said it plainly: connect() failed (111: Connection refused) to an address nobody was listening on any more. One docker compose restart nginx and the site was back. A minute of 502, at an hour when nobody was reading.
Then the settings page: Client: PhpRedis (v6.3.0). PHP went from 8.3.33 to 8.3.35 on the way, a side effect of --pull I hadn't asked for and don't mind.
asking the DNS instead
I could have left it there, with a line in the commit message saying "restart nginx afterwards" and the certainty of rediscovering it in December. Doing things blind and then chasing them is not a method, so I fixed the actual cause.
The container IPs on a Docker network are not meant to be stable, and tonight proved it both ways: three recreations, two different addresses, one reused. What Docker does keep true is the name, through its internal DNS. So nginx now has to ask:
resolver 127.0.0.11 valid=10s ipv6=off;
set $php_upstream wordpress:9000;
...
fastcgi_pass $php_upstream;
With the upstream in a variable nginx cannot bake the address in at startup; it resolves at request time and re-asks the DNS at most every ten seconds. Backup of the old file, new one built aside and diffed, nginx -t inside the container before the restart. Then two more recreations of the PHP container, and no 502 either time.
I'll be honest about what that proves: on one of the two the container came back with the same IP it had before, so the old configuration would have survived that one too. The mechanism is the documented one and the config test passed; the clean proof will come at the next patching, when the address moves and nobody has to restart anything.
is it faster
I don't know yet, and I'm writing that down so I can't pretend otherwise later. The theory is sound: the serialisation and the protocol now happen in C instead of PHP, and the site does about sixty Redis operations a second. The bottleneck of this site is not the Redis client though - it is PHP rendering archive pages for machines that will never read them. The gain may well be in the noise.
So the deal I made with myself is: commit now, measure over the next evenings, and if the CPU of the PHP container doesn't move at comparable traffic, go back to the official image. A derived image you cannot justify with a number is just maintenance.
I also flushed the cache by hand, which means the next few hours will be all misses and prove nothing. Tomorrow, then.