Redis object cache for WordPress on a VPS
Set up Redis as a WordPress object cache on your own VPS: bind it to localhost, size maxmemory, pick an eviction policy, and verify the cache is real.
What a Redis object cache does for WordPress
A Redis object cache for WordPress stores the results of database queries in memory, so the next request reads them from Redis instead of asking MySQL again. WordPress already has an object cache in core, WP_Object_Cache, but it lives in PHP memory and is thrown away when the request ends. A drop-in file replaces it with one that talks to Redis, so the cache survives from one request to the next.
Object caching is not page caching, and the difference decides whether this guide is worth your time. A page cache stores the finished HTML of a URL and serves it again without running PHP at all. That is faster than anything Redis can do, and it works for visitors who are not logged in. The moment somebody logs in, puts an item in a cart, or opens the admin, the page cache steps aside and WordPress runs the whole request: bootstrap, plugins, queries. An object cache makes that request cheaper. It is the tool for the traffic a page cache cannot touch: logged-in sessions, carts, checkout, wp-admin. On a WooCommerce shop that is most of the expensive traffic.
The two stack, and on a busy site both belong. Be clear about which problem you are fixing. A brochure site with anonymous readers gets almost all of its speed from a page cache, and adding Redis to it changes very little.
One honest limit before you start. An object cache does not make a slow query fast. It removes the repeat of a query that already ran. The first request after a miss pays full price, so a plugin running an unindexed query still runs it once per cache lifetime.
What you need first
- A Linux VPS with a shell and
sudo. No control panel is required. - WordPress served by PHP-FPM, for example on a LAMP stack on Ubuntu 24.04.
- WP-CLI on the box. Every step here has an admin-screen equivalent, and the shell version is faster.
- Redis on the same machine as PHP. Latency is the whole point, and a network hop undoes it.
Commands below are written for Ubuntu 24.04 with PHP 8.3 and the www-data web user. Adjust the PHP version and the user to match your box. Run the wp commands from your WordPress directory, the one holding wp-config.php.
Install Redis and the PHP extension
sudo apt update
sudo apt install -y redis-server php-redis
sudo systemctl enable --now redis-server
redis-cli pingredis-cli ping should answer PONG. If it prints Could not connect to Redis at 127.0.0.1:6379: Connection refused, the server is not running, so read systemctl status redis-server before going further.
php-redis is PhpRedis, the C extension from PECL. It is faster than Predis, which is pure PHP, and the plugin uses it automatically when it is present. PHP-FPM loads extensions at start, so a new one is invisible until you restart the pool.
sudo systemctl restart php8.3-fpm
php -m | grep redisBe careful with that last check: php -m lists the modules of the command line PHP, and FPM can load a different set. The check that counts is the plugin's own diagnostics, further down.
As of August 2026, Ubuntu 24.04 packages Redis 7.0.15, which is fine for an object cache. If you want a current release instead, Redis publishes its own APT repository.
sudo apt install -y lsb-release curl gpg
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
sudo chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt update
sudo apt install -y redisIf your distribution ships Valkey, the fork started after the 2024 licence change, it speaks the same protocol and everything below applies unchanged.
Bind Redis so nothing else can reach it
Redis has no password by default. Anything that can open a connection to port 6379 can read every cached value and run FLUSHALL. Instances exposed to the internet get found by scanners within hours, so the network setting comes before the tuning.
Open /etc/redis/redis.conf and confirm these lines:
bind 127.0.0.1 -::1
protected-mode yesThen check what is really listening, because the config file is a claim and ss is the evidence.
sudo ss -lntp | grep 6379127.0.0.1:6379 is what you want. 0.0.0.0:6379 means Redis is answering on the public interface: fix the bind line and restart.
When PHP and Redis sit on the same box, a Unix socket is better than loopback TCP. There is no TCP stack in the path, and access is decided by file permissions instead of by a firewall rule you might later change.
unixsocket /run/redis/redis-server.sock
unixsocketperm 770The socket is owned by the redis user and group, so the web user has to join that group.
sudo systemctl restart redis-server
sudo usermod -aG redis www-data
sudo systemctl restart php8.3-fpm
redis-cli -s /run/redis/redis-server.sock pingThat must also print PONG. Could not connect to Redis at /run/redis/redis-server.sock: Permission denied means the group did not take effect. Check id www-data, and remember a running PHP-FPM keeps the groups it had at start, which is why the restart is in the list. Leave TCP enabled until the socket is proven, or a typo takes away both paths at once.
How much memory should Redis get?
Derive the number from your own box. Redis with no maxmemory grows until the kernel runs out and the OOM killer ends a process, usually the largest one, which on a WordPress server is often MySQL. journalctl -k | grep -i "out of memory" shows that kill after the fact, and the site is down by then.
Start from total RAM and subtract. MySQL or MariaDB reserves innodb_buffer_pool_size plus per-connection buffers. PHP-FPM costs pm.max_children multiplied by the real resident size of one worker, commonly 64 MB to 128 MB on a plugin-heavy site. The kernel and the web server want a few hundred megabytes. What is left is your ceiling, and Redis gets a part of it.
A worked budget for a 4 GB VPS running one shop
These are example figures, not measurements from your server. Replace each one with the value your box reports.
- MariaDB with a 1 GB buffer pool: 1024 MB
- PHP-FPM, 10 workers at 96 MB each: 960 MB
- Kernel, nginx or Apache, sshd, logging: 512 MB
- Left over: roughly 1.5 GB
A maxmemory of 256 MB is a sensible opening bid there. It leaves real headroom, and a single WordPress site rarely needs more.
Now measure instead of guessing. After a day of real traffic:
redis-cli info memory | grep -E 'used_memory_human|maxmemory_human|maxmemory_policy'
redis-cli dbsizeIf used_memory_human sits far below your limit, lower the limit and give the RAM back to MySQL, which will use it better. If it pins at the limit and evicted_keys climbs all day, raise it. Set the value in /etc/redis/redis.conf.
maxmemory 256mb
maxmemory-policy allkeys-lruredis-cli config set maxmemory 256mb applies right now and is forgotten at the next restart, the same trap as a bare sysctl -w. Edit the file, then sudo systemctl restart redis-server, then read the value back. A second wall is worth having: a MemoryMax cap on the systemd unit stops a misconfigured Redis from taking the box down. Set it above maxmemory, never equal to it, because a cgroup limit kills the process rather than evicting a key. If Redis runs in a container beside WordPress, the same figure belongs in the memory limits in your Compose file, and the same reasoning drives the choice of running the database in Docker or on the host.
Choose the eviction policy on purpose
A fresh Redis defaults to noeviction. Check yours:
redis-cli config get maxmemory-policyUnder noeviction, a full instance stops accepting writes and answers with this:
(error) OOM command not allowed when used memory > 'maxmemory'.That single line is the worst failure mode in this guide, because the site does not go down. It gets slower. Every cache write fails, so WordPress goes back to the database for the value, then tries to store it again on the next request and fails again. The site now pays all of its original database work plus a round trip to Redis for every key. Nothing in the WordPress admin tells you this is happening. The string turns up in the PHP error log, so grep for OOM command not allowed when a site gets slower after you added a cache.
allkeys-lru is the right default here. Redis drops the least recently used key when memory is tight, which is exactly what an object cache wants, because every value in it is a copy of data that still exists in MySQL. Losing a key costs one query. Refusing a write costs every query, on every request, until somebody notices.
Avoid the volatile-* policies for this job. They only consider keys that carry an expiry, and Redis documents that they behave like noeviction when no key has one. WordPress stores most object cache entries with no TTL, so volatile-lru on an object cache can fill up and start refusing writes. allkeys-lfu is a fair alternative if your traffic hits a small set of keys very often, since it evicts by frequency rather than recency. Pick one deliberately and write down why.
Persistence: leave it off unless you have a reason
The packaged redis.conf enables RDB snapshots with lines like save 900 1, and leaves the append-only file off. For a pure object cache, snapshots buy nothing. The data is regenerable by definition, and a cache restored from a twenty-minute-old file is a set of stale values that WordPress will trust.
Snapshots also cost. BGSAVE forks the process, and copy-on-write means memory use can rise sharply while the child writes. On a small VPS that shows up in the Redis log:
Can't save in background: fork: Cannot allocate memoryand often this warning at startup, which is Redis telling you the fork is likely to fail later:
WARNING overcommit_memory is set to 0! Background save may fail under low memory condition.To turn snapshots off, set an empty save schedule in /etc/redis/redis.conf, restart, and confirm the value came back empty.
save ""sudo systemctl restart redis-server
redis-cli config get saveKeep persistence only if the same instance holds something you cannot rebuild, such as a job queue or rate-limit counters. In that case split the two. A cache wants keys evicted and durable data wants keys kept, and maxmemory plus eviction applies to the whole instance, not to one database index. Two instances on two sockets is the clean answer.
Install the plugin, and understand the drop-in
wp plugin install redis-cache --activate
wp redis enable
wp redis statuswp redis enable prints Object cache enabled. on success. What it actually does is copy wp-content/plugins/redis-cache/includes/object-cache.php to wp-content/object-cache.php. That copy is the drop-in, and the drop-in is the part that does the work. WordPress loads wp-content/object-cache.php very early, before any plugin code runs, which is how the cache is available for the whole request. An active plugin with no drop-in in place caches nothing.
The failure messages tell you which half broke. Object cache could not be enabled. means the copy failed, so wp-content is not writable by the user running WP-CLI. A foreign object cache drop-in was found. means another caching plugin already owns that filename, and the fix is wp redis update-dropin. A message ending Redis server is unreachable: followed by the client error means the connection settings are wrong, so go back to redis-cli ping.
If the copy failed on permissions, place it by hand and give it to the web user.
cp wp-content/plugins/redis-cache/includes/object-cache.php wp-content/object-cache.php
sudo chown www-data:www-data wp-content/object-cache.phpRemoving the plugin does not remove the drop-in. Run wp redis disable first, which prints Object cache disabled. and deletes the file. Delete the plugin directory while the drop-in stays behind and the site keeps running old cache code with no plugin to update it.
Connection settings in wp-config.php
Add these above the line that reads /* That's all, stop editing! */, because constants defined after it are too late.
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_DATABASE', 0 );
define( 'WP_REDIS_PREFIX', 'shop_prod:' );For the Unix socket, set the scheme and the path. Host and port are then ignored.
define( 'WP_REDIS_SCHEME', 'unix' );
define( 'WP_REDIS_PATH', '/run/redis/redis-server.sock' );
define( 'WP_REDIS_PREFIX', 'shop_prod:' );WP_REDIS_MAXTTL forces an expiry on every key, in seconds. You do not need it with allkeys-lru, and it is useful if you want a hard upper bound on how stale a cached value can be.
One Redis, several sites: prefixes and databases
Redis gives you sixteen numbered databases by default, and one flat keyspace inside each. Two WordPress installs pointed at database 0 with no prefix write the same key names into the same space, so one site can read the other's options and serve them. Give every site its own prefix.
define( 'WP_REDIS_PREFIX', 'shopA_prod:' );
define( 'WP_REDIS_DATABASE', 1 );The prefix separates the key names. The database index separates the keyspaces, which matters at flush time: emptying one index leaves the others alone. The plugin also documents WP_REDIS_SELECTIVE_FLUSH, which deletes only the keys matching your prefix instead of the whole database, at the cost of scanning for them.
What prefixes and indexes do not separate is memory. maxmemory and the eviction policy apply to the instance as a whole, so a busy site can push a quiet site's keys out and neither one reports it. Sites that must not affect each other need separate Redis instances, each with its own socket and its own limit.
Keep staging out of production's cache
A staging site is usually a copy of the production files and database, which means it is a copy of wp-config.php carrying the same prefix and the same database index. Point it at the same Redis and it writes production's keys with staging values. A test price or a changed option then appears on the live site with no deploy and no trace.
Salt each environment by hand. In staging's wp-config.php:
define( 'WP_REDIS_PREFIX', 'shop_staging:' );
define( 'WP_REDIS_DATABASE', 5 );Better still, give staging its own Redis instance, or no object cache at all. define( 'WP_REDIS_DISABLED', true ); turns the cache off at runtime and leaves the drop-in in place, which is also the fastest way to prove that a bug is or is not the cache.
Older tutorials set WP_CACHE_KEY_SALT for this. The plugin's readme marks that constant deprecated and replaced by WP_REDIS_PREFIX, so use the new name.
Verify it instead of trusting it
Start with the plugin's own diagnostics.
wp redis statusThe line that matters most is Drop-in. Drop-in: Valid means WordPress is loading this plugin's file. Drop-in: Not installed means the copy never happened and the site has no persistent cache, however green the admin screen looks. Status reports the connection, and Client names the extension in use, which is where you confirm PhpRedis rather than Predis.
Then ask WordPress core directly, because it does not care what the plugin thinks.
wp eval 'var_dump( wp_using_ext_object_cache() );'bool(true) means core is talking to an external object cache.
Then prove that keys are arriving, with the prefix you configured.
redis-cli -n 0 dbsize
redis-cli -n 0 --scan --pattern 'shop_prod:*' | headdbsize rising while you click around the site is the proof. Zero keys with a valid drop-in means the connection is failing quietly, or the prefix is not the one you think it is.
Finally, look at what Redis measures for you.
redis-cli info stats | grep -E 'keyspace_hits|keyspace_misses|evicted_keys|expired_keys'The hit ratio is keyspace_hits / (keyspace_hits + keyspace_misses), and Redis' documentation gives that formula. Read it with two cautions. The counters cover the whole instance since its last restart, so they mix every site and every application sharing it. And the ratio just after a flush or a restart means nothing, because the cache is still filling. Let it run through a normal traffic day.
Do not compare your number to a hit rate or a query count published by a hosting company. Those describe their sites and their plugin set. The figure that matters is your own, measured before and after on a page a page cache cannot serve.
curl -o /dev/null -s -w '%{time_starttransfer}\n' -b cookies.txt https://example.com/my-account/Run that with a logged-in cookie jar, several times, with the cache off (WP_REDIS_DISABLED) and then on. That difference is your result.
When Redis makes WordPress slower
A full instance with the wrong policy is the big one, covered above: OOM command not allowed when used memory > 'maxmemory'. in the log, and a site paying for both the database and the cache.
Redis on another host is the second. WordPress makes hundreds of object cache calls in one request. If a request makes 500 calls and each round trip costs 1 ms, that is half a second of waiting that a local socket would not have. Keep Redis on the same box, or on a private network with sub-millisecond latency.
A huge autoloaded options table is the third, and it is common on old sites. WordPress caches all autoloaded options as one key, so a megabyte of them crosses the connection on every single request. Measure it:
wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024) AS kb FROM wp_options WHERE autoload IN ('yes','on','auto','auto-on');"WordPress 6.6 added new autoload values, so an older query matching only 'yes' under-reports on a modern install. Anything past a megabyte is a problem to fix in the options table, not in Redis.
A restart empties everything, so the minutes after systemctl restart redis-server are all misses and all database work. Restart when traffic is low. And an object cache does not stop wp-cron.php firing on visitor page loads, which is its own source of slow requests: move WP-Cron to a real system cron job while you are in here.
Housekeeping
Flush after a deploy that changes options or theme code with wp cache flush. Run wp redis update-dropin after a plugin update if the drop-in did not update itself, since a drop-in from an older plugin version against a newer plugin is a real source of odd behaviour. Watch a live server with redis-cli --stat, which prints one line per second. redis-cli monitor prints every command and costs real CPU on a busy instance, so use it for a few seconds while you reproduce something, then stop it.
One last number worth knowing: redis-cli info clients reports connected_clients. PHP-FPM holds a connection per worker, so that figure should track your pm.max_children, not exceed it by an order of magnitude. If it does, something is opening connections and not closing them.
FAQ
Do I still need a page cache if I run a Redis object cache?
Yes, for anonymous traffic. A page cache serves stored HTML without running PHP, which is always cheaper than running WordPress with a warm object cache. The object cache handles the requests a page cache must skip: logged-in users, carts, checkout, and wp-admin. On a shop or a membership site both are worth running. On a site whose visitors never log in, the page cache does nearly all of the work.
How much memory should I give Redis for WordPress?
Derive it from your own box rather than copying a figure. Take total RAM, subtract the MySQL buffer pool and per-connection buffers, subtract pm.max_children multiplied by the resident size of one PHP-FPM worker, subtract a few hundred megabytes for the kernel and web server. Give Redis a part of what remains, then check used_memory_human in redis-cli info memory after a day of traffic and adjust. A single WordPress site usually settles in tens of megabytes, so a 256 MB maxmemory is a generous start on a 4 GB server.
Why did my site get slower after enabling the Redis object cache?
The usual cause is a full instance running the noeviction policy. Redis refuses new writes and returns OOM command not allowed when used memory > 'maxmemory'., so WordPress falls back to the database for every value and pays a wasted Redis round trip on top. Check redis-cli config get maxmemory-policy, set allkeys-lru, and confirm maxmemory is not tiny. The other common causes are a Redis server on a remote host, where hundreds of round trips per request add up, and a multi-megabyte autoloaded options value that crosses the connection on every request.
Can several WordPress sites share one Redis server?
They can, with care. Give each site a unique WP_REDIS_PREFIX so key names cannot collide, and a separate WP_REDIS_DATABASE index so flushing one site does not empty another. What they still share is memory: maxmemory and eviction apply to the whole instance, so a busy site can evict a quiet site's keys. Sites that must not affect each other need separate Redis instances with their own limits.
Is it safe to delete wp-content/object-cache.php?
Yes. It is a drop-in, not part of WordPress core, and removing it returns WordPress to its built-in per-request cache. The site keeps working and simply does more database queries. Prefer wp redis disable, which deletes the file cleanly and reports Object cache disabled. Deleting it by hand is the right emergency move if Redis is down or misbehaving and you cannot reach the admin.