Redis Caching for PHP, Measured: 86% Fewer Queries, No Faster Pages

  • Tested: Ubuntu 24.04 LTS
  • 4 session configs compared
  • 13 of 20 writes lost by default
  • 6 September 2026
  • 8 min read

Published 6 September 2026. Measured on one live server we operate — 2 vCPU, 7.8 GiB, Ubuntu 24.04, PHP 8.3.6, MySQL 8.0.46, Redis 7.0.15 — by adding a Redis object cache to a real WordPress install, benchmarking it, then removing it. The conclusion is not the one the title of most Redis tutorials promises.

1. The Object Cache Removed 86% of the Queries and Made Nothing Faster

The standard advice for a slow PHP application is to put Redis in front of the database. We did exactly that on a WordPress install that was serving 64 SQL queries per page, using the same Redis object cache drop-in everyone recommends, and instrumented the request to see where the time actually went.

SQL queriesTime in SQLTotal page time
No object cache6428.0 ms (23%)123.7 ms
Redis object cache911.1 ms (9%)126.2 ms

Fifty-five queries eliminated. Seventeen milliseconds of database time saved. And the page took 2.5 milliseconds longer.

The saved database time was spent somewhere else: Redis round trips, plus serialising and unserialising every cached value. On a server where MySQL is on localhost with a warm buffer pool, those 64 queries were not expensive to begin with. Replacing them with 9 queries and a few hundred Redis calls is a lateral move.

Throughput agrees. Three runs before and three after, at concurrency 10:

bash
Without object cache:  14.33   13.78   15.01  req/s
With Redis:            15.28   15.37   14.44  req/s

The two ranges overlap. Whatever difference exists is smaller than the run-to-run variation of the measurement, which is the technical way of saying there is no difference.

Measured

23% of the page was database time before we started. That is the ceiling on what any database cache can return, and it assumes the cache is free, which it is not. When someone promises a 10× speedup from Redis, ask what fraction of their page was database time. If it was 23%, the arithmetic does not allow it.

Where a Redis object cache does pay

The result above is specific and it is worth saying exactly what it does not cover. A Redis object cache earns its place when:

  • The database is on another host. Sixty-four queries at 0.4 ms each locally is 26 ms. The same 64 queries at 1.5 ms of network round trip each is 96 ms, and removing 55 of them saves real time.
  • Several application servers share a cache. That is the actual design purpose; a single VPS does not need it.
  • Individual queries are genuinely expensive. Caching one aggregate that takes 400 ms is worth far more than caching 55 that take 0.4 ms.
  • The database is the bottleneck under load, not PHP. Measure before assuming; on this server it was not.

Reducing database load by 86% is still worth something on a machine where MySQL serves several sites — it just does not show up as a faster page for the visitor, which is what was promised.

2. Redis Sessions Are Faster Because They Lose Data

The second half of the standard advice is to move PHP sessions from files to Redis, and this one has a real mechanism behind it. PHP’s file session handler locks the session file for the duration of the request, so two concurrent requests carrying the same session serialise. Any application with parallel AJAX calls hits this constantly.

We built a minimal endpoint — start a session, do 20 ms of work, write, close — and hammered it at concurrency 10 with a single shared session cookie:

Session handlerSame session, c=10Different sessions, c=10
Files48.90 req/s (P50 204 ms)146.57 req/s (P50 63 ms)
Redis, defaults131.31 req/s (P50 71 ms)154.59 req/s (P50 54 ms)

2.7 times the throughput under contention. That is the number the tutorials quote, and we reproduced it. It is also, on the default configuration, a bug.

The test that shows why

The file handler is slower because it holds a lock. The Redis handler does not hold one — redis.session.locking_enabled defaults to 0. To measure what that costs, we fired 20 concurrent requests that each read a session counter, waited 30 ms, and wrote it back incremented. With correct locking the counter must reach 21.

php
<?php
session_start();
$v = $_SESSION['c'] ?? 0;
usleep(30000);              // window in which a second request reads the same value
$_SESSION['c'] = $v + 1;
echo $_SESSION['c'];
ConfigurationCounter after 20 incrementsWrites lostThroughput
Files21 — correct048.90 req/s
Redis, defaults813 of 20131.31 req/s
Redis, locking enabled1110 of 2080.80 req/s
Redis, locking + 1 s lock budget21 — correct046.06 req/s

Read the last row against the first. A correctly configured Redis session handler is slower than files on this server. Every bit of the 2.7× came from not locking, which is to say from discarding two thirds of the concurrent session writes.

Warning

on a shopping cart, a multi-step form, or anything with parallel AJAX writing to $_SESSION, this is silent data loss. Nothing is logged and nothing errors — the counter is simply wrong. If you have moved sessions to Redis and never set redis.session.locking_enabled=1, you have this behaviour in production right now.

Enabling the lock is not enough either

The third row of that table is the trap inside the trap. With locking switched on, the counter reached 11 instead of 21 — still losing half the writes. The reason is the retry budget:

bash
redis.session.lock_retries   = 10       # default
redis.session.lock_wait_time = 2000     # microseconds, default
                              ↓
             10 × 2000 µs = 20 ms of total waiting

Our request holds the session for 30 ms. After 20 ms of waiting, PHP gives up on the lock and proceeds without it, silently. Any request slower than the budget behaves as though locking were off. The working configuration:

bash
; The budget must exceed the slowest request that touches $_SESSION.
; 200 × 5000 µs = 1 second.
redis.session.locking_enabled = 1
redis.session.lock_retries    = 200
redis.session.lock_wait_time  = 5000
redis.session.lock_expire     = 30      ; seconds, so a crashed request cannot
                                        ; hold the lock forever

That configuration produced 21 out of 21 — and 46.06 requests per second, marginally below the file handler it replaced.

3. Two Configuration Faults We Hit on the Way

nginx silently truncates PHP_VALUE at the first equals sign

We pointed sessions at a dedicated Redis database so they could not collide with the object cache, by passing the save path through nginx:

bash
fastcgi_param PHP_VALUE "session.save_handler=redis
session.save_path=tcp://127.0.0.1:6379?database=4";

What PHP actually received:

conf
handler=redis  path=tcp://127.0.0.1:6379?database

The =4 is gone. PHP_VALUE is parsed as name=value pairs, so a value containing its own equals sign is cut. The database selector was dropped and every session landed in database 0 — the default, shared with anything else using Redis on that host. No warning, no error; the sessions worked, they were simply in the wrong place.

Set save paths with query parameters in a PHP-FPM pool file or an .ini, never through PHP_VALUE. And verify it, because the failure is invisible:

php
<?php echo ini_get('session.save_path'), "\n";

Redis ships with no memory limit and refuses to evict

The default Redis configuration on this Ubuntu install, and on most distributions:

bash
$ redis-cli INFO memory | grep -E 'maxmemory_human|maxmemory_policy'
maxmemory_human:0B
maxmemory_policy:noeviction

No ceiling, and when memory does run out, nothing is evicted — writes are refused instead. On a 7.8 GiB VPS running several sites, a cache that grows without bound is a machine that eventually gets an out-of-memory kill, and the process the kernel chooses may not be Redis.

bash
# /etc/redis/redis.conf — a cache should behave like a cache
maxmemory 256mb
maxmemory-policy allkeys-lru
Warning

allkeys-lru is correct for a pure cache and wrong for sessions. Under memory pressure it will evict session keys and log users out at random. If Redis holds both, either give sessions their own instance, or use volatile-lru and make sure only cache entries carry a TTL.

4. What Actually Made PHP Fast on This Server

Before reaching for Redis, check the thing that genuinely delivers a large multiplier, because it was already doing the work here:

bash
$ php -i | grep -E '^opcache.enable |^opcache.memory_consumption'
opcache.enable => On => On
opcache.memory_consumption => 128 => 128

Loading the WordPress core — parsing and executing PHP before a single template runs — was 446 ms without OPcache in our CLI profile, against a complete page render of 123.7 ms under FPM with it. That gap is the real story of PHP performance, and it is a setting you either have or do not.

The order of operations for a slow PHP application on a small server, based on what we measured:

  • Confirm OPcache is on. Nothing else in this list comes close.
  • Measure where the time goes before choosing a fix. Ours was 23% database, 77% PHP — which ruled out the database cache before we installed it.
  • Put a page cache in front. On this same server, an nginx microcache took WordPress from 15 to 5,900 requests per second — a factor of 384, against Redis’s zero.
  • Then consider Redis, for a remote database, several application servers, or genuinely expensive individual queries.

5. Frequently Asked Questions

Does Redis object cache speed up WordPress?

On our single server it did not. Queries per page fell from 64 to 9 and database time from 28 ms to 11 ms, but total page time went from 123.7 ms to 126.2 ms — slightly worse, because Redis round trips and serialisation cost more than the local MySQL queries they replaced. It helps when the database is on another host.

Are Redis PHP sessions faster than file sessions?

Only with locking disabled, which is the default and which loses data. We measured 131 requests per second against 49 for files, but 13 of 20 concurrent session writes were silently discarded. Configured to lock correctly, Redis gave 46 requests per second — marginally slower than the file handler.

Why is my PHP session data disappearing with Redis?

Because redis.session.locking_enabled defaults to 0, so concurrent requests overwrite each other. Enabling it is not sufficient either: the default budget of 10 retries at 2000 microseconds gives only 20 ms of waiting, after which PHP proceeds without the lock. Raise the retries to cover your slowest request.

What maxmemory should I set for Redis on a small VPS?

Something, because the default is unlimited with noeviction, meaning Redis grows until the machine runs out of memory and then refuses writes. Set an explicit ceiling such as 256 MB with allkeys-lru for a pure cache. If the same instance also stores sessions, evictions will log people out.

Will Redis fix a slow CodeCanyon script on a $5 VPS?

Only if the database is what is slow, which you must measure rather than assume. Our page was 77% PHP execution and 23% database, so no database cache could have delivered more than a small improvement. Check OPcache first, then add a page cache — that is where the order of magnitude lives.

6. The Short Version

  • Redis object cache: 64 queries → 9, and the page got 2.5 ms slower. No throughput gain.
  • Redis sessions look 2.7× faster because they do not lock. They lost 13 of 20 concurrent writes.
  • Locked correctly, Redis sessions were slower than files on one server.
  • PHP_VALUE truncates values at =, so a Redis save path with ?database=4 silently uses database 0.
  • Redis defaults to unlimited memory and noeviction. Set a ceiling.

For the page cache that did produce the order of magnitude, our Ghost versus WordPress benchmark covers the nginx microcache configuration and what it does to both platforms, and the capacity model puts these numbers beside six other applications on the same machine.