If you're running a couple of PHP apps on one VPS - maybe a Laravel API, a legacy CodeIgniter app, and a WordPress install sharing the same box - you've probably hit the point where PHP's default file-based sessions start creaking under load. Memcached is the classic fix, but it trips people up in two very specific ways: the PHP extension naming is a mess, and by default it's happy to sit there listening on a port with zero authentication. Here's how to install it properly, wire it into PHP, and avoid both traps.
Symptom: Slow Apps Under Concurrent Load, or a "Class Memcached Not Found" Crash
There are two very different situations that lead people here:
- Session lock contention. A user opens your app in two tabs, or your frontend fires several AJAX calls at once. PHP's default file-based session handler locks the session file for the duration of each request, so those requests queue up and run one after another instead of in parallel - even though nothing else on the server is busy.
- An app that explicitly requires Memcached. Some legacy apps, some Laravel configs, and a fair number of self-hosted tools (Magento, older Symfony projects) list Memcached as a cache or session driver in their config, and just fail outright -
Class "Memcached" not found- if the extension isn't installed.
Both point to the same fix, so let's set it up correctly the first time.
Cause: No Shared, In-Memory Cache Between PHP Workers
PHP-FPM runs a pool of worker processes. Each one is a separate process with its own memory space - there's no built-in way for worker #3 to see data that worker #1 just computed. Two things suffer from this:
- Sessions. The default
session.save_handler = fileswrites each user's session to a flat file under/var/lib/php/sessions. Concurrent requests from the same session have to wait for a file lock to release, which is fine at low traffic and painfully slow once a user's browser fires off parallel requests. - Object/data caching. Without a shared cache, every worker independently re-runs the same expensive lookup - a pricing calculation, a third-party API call, a heavy SQL query - because it has no way of knowing another worker already did it thirty seconds ago.
Memcached solves both by sitting outside the PHP process pool as its own daemon, holding data in RAM that any worker (or any app on the box) can read and write.
Fix: Install and Wire Up Memcached
1. Install the Memcached Daemon
On Ubuntu/Debian:
sudo apt update
sudo apt install memcached libmemcached-tools -yOn AlmaLinux/RHEL:
sudo dnf install memcached libmemcached -y
sudo systemctl enable --now memcached2. Bind It to Localhost Only
Memcached ships with no authentication in its default configuration. If it's reachable from the public internet, anyone can read, write, or flush your cache - and UDP-enabled instances have been abused for DDoS amplification attacks in the past. Edit the config:
sudo nano /etc/memcached.confMake sure these lines are set (Ubuntu/Debian path shown; on RHEL it's /etc/sysconfig/memcached):
# Listen only on localhost
-l 127.0.0.1
# Memory limit in MB - size this to what you can spare, not all your RAM
-m 128
# Disable UDP entirely (it's the vector abused for amplification attacks)
-U 0Restart to apply:
sudo systemctl restart memcachedThen confirm it's only listening locally:
sudo ss -tlnp | grep memcached
# should show 127.0.0.1:11211, not 0.0.0.0:112113. Install the Right PHP Extension
This is where most setups go wrong. There are two unrelated PHP extensions with almost identical names:
| Extension | Backed by | Status | Use it? |
|---|---|---|---|
php-memcache | Custom protocol implementation | Unmaintained, abandoned since PHP 5.x era | No |
php-memcached | libmemcached | Actively maintained, supports igbinary, SASL | Yes |
Install the correct one, matched to whichever PHP version your app actually runs (check with php -v first if you have multiple versions installed via update-alternatives or Remi/Ondřej repos):
# Ubuntu/Debian, PHP 8.3 example
sudo apt install php8.3-memcached -y
# AlmaLinux/RHEL
sudo dnf install php-pecl-memcached -yRestart PHP-FPM so the extension actually loads into the worker pool:
sudo systemctl restart php8.3-fpmVerify it loaded:
php -m | grep -i memcachedIf that's blank but apt list --installed | grep memcached shows it's installed, you almost certainly restarted the wrong PHP-FPM version, or your CLI PHP binary is a different version from the one Nginx/Apache hands requests to.
4. Point PHP Sessions at Memcached
To fix the session-locking problem specifically, edit your pool's php.ini or the site's FPM pool config:
session.save_handler = memcached
session.save_path = "127.0.0.1:11211"For a single app, dropping this into /etc/php/8.3/fpm/conf.d/99-memcached-sessions.ini works well. If you're running multiple sites on the box and only want one of them on Memcached sessions, set it in that site's own FPM pool file under [pool-name] instead of globally:
php_admin_value[session.save_handler] = memcached
php_admin_value[session.save_path] = "127.0.0.1:11211"Restart PHP-FPM again after this change.
5. Confirm It's Actually Being Used
# Live stats - watch curr_items and get_hits climb as traffic hits the app
memcached-tool 127.0.0.1:11211 stats
# Or a quick raw check
printf "stats\r\n" | nc 127.0.0.1 11211 | head -20If curr_connections stays flat at 1 and cmd_get/cmd_set never move while you're clicking around the app, PHP isn't actually talking to Memcached yet - double-check step 3 and 4 rather than assuming it's working.
Prevention
- Never expose port 11211 externally. Confirm your firewall (ufw, firewalld, or your provider's security group) blocks it by default, don't rely on the
-l 127.0.0.1bind alone - defense in depth matters here. - Size
-mdeliberately. Memcached will happily use exactly what you give it and evict the oldest data (LRU) once it's full. Giving it too much starves MySQL, PHP-FPM, and everything else sharing the VPS's RAM. - Treat it as disposable. Memcached keeps everything in RAM only - a restart, an OOM kill, or a reboot wipes it clean. Never store anything there that isn't safe to lose (sessions being logged out is annoying, not catastrophic; don't use it as your only copy of anything that matters).
- Watch eviction counts.
memcached-tool 127.0.0.1:11211 statsshows anevictionscounter. If it's climbing steadily, your-mvalue is too small for your working set and you're thrashing rather than caching.