Memcached is a simple, multithreaded in-memory key-value cache. It has no persistence, no replication and no complex data types, and that simplicity is exactly why it is fast and predictable for caching query results, rendered pages and sessions. In this tutorial you will install Memcached on Ubuntu 24.04, configure its memory, connection and thread limits, keep it private, use it from Python and PHP, and read its statistics to check that the cache is effective.

Prerequisites

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 1 GB of RAM.
  • A non-root user with sudo privileges.
  • Optionally, PHP-FPM if you want to follow the PHP session example in Step 6.

Step 1 - Installing Memcached

Install Memcached from the Ubuntu repository together with netcat-openbsd, which you will use to query its statistics:

sudo apt update
sudo apt install memcached netcat-openbsd

The package creates the memcached systemd service, starts it and enables it at boot. Check its status:

sudo systemctl status memcached --no-pager
● memcached.service - memcached daemon
     Loaded: loaded (/usr/lib/systemd/system/memcached.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-25 11:02:44 UTC; 6s ago
...

Confirm which address and port it listens on:

sudo ss -ltnp | grep 11211
LISTEN 0      1024       127.0.0.1:11211      0.0.0.0:*    users:(("memcached",pid=3120,fd=26))

By default Memcached on Ubuntu only listens on 127.0.0.1, so it is not reachable from other machines.

Step 2 - Configuring memory, connections and threads

On Ubuntu, the service reads its command-line options from /etc/memcached.conf, one option per line. Open it:

sudo nano /etc/memcached.conf

Adjust or add these lines. Options that are commented out with # use their built-in default:

# Run as a daemon and log to this file
-d
logfile /var/log/memcached.log

# Memory for cached items, in megabytes (default 64)
-m 512

# TCP port, and disable UDP
-p 11211
-U 0

# User and listen address
-u memcache
-l 127.0.0.1

# Maximum simultaneous connections (default 1024)
-c 2048

# Worker threads (default 4)
-t 4

-P /var/run/memcached/memcached.pid

What each setting controls:

  • -m: the memory budget for stored items. When it is full, Memcached evicts the least recently used items to make room. The default of 64 MB is too small for most real workloads. Give Memcached what is left after the operating system, your application and your database have enough memory; on a 4 GB server that also runs a database, 512 MB to 1 GB is a sensible start.
  • -c: the maximum number of client connections. Size it for the number of application workers on all servers that connect, with some margin. For example, 3 servers with 100 PHP-FPM workers each, each holding one persistent connection, need at least 300.
  • -t: worker threads. The default of 4 is right for most servers. Raise it only on machines with many cores that handle very high request rates; more threads than cores does not help.
  • -U 0: disables the UDP listener. UDP is rarely used by clients and exposed UDP Memcached servers have been abused for large DDoS amplification attacks. Recent versions already disable UDP by default; setting it explicitly documents the intent.
  • -l: the listen address. Keep 127.0.0.1 when the application runs on the same server.

Restart the service to apply the changes:

sudo systemctl restart memcached

Verify the effective settings by asking Memcached itself. The quit command makes the server close the connection, so nc exits on its own:

printf 'stats settings\r\nquit\r\n' | nc 127.0.0.1 11211 | grep -E "maxbytes|maxconns|udpport|num_threads|item_size_max"
STAT maxbytes 536870912
STAT maxconns 2048
STAT udpport 0
STAT num_threads 4
STAT item_size_max 1048576

536870912 bytes is 512 MB. item_size_max shows the largest value Memcached accepts, 1 MB by default. You can raise it with -I 2m, but large values are usually better stored elsewhere or compressed by the client.

Step 3 - Testing Memcached with the text protocol

Memcached speaks a simple text protocol, which makes it easy to test by hand. Store a key named greeting with flags 0, a TTL of 300 seconds and a 5-byte value, read it back and delete it:

printf 'set greeting 0 300 5\r\nhello\r\nget greeting\r\ndelete greeting\r\nquit\r\n' | nc 127.0.0.1 11211
STORED
VALUE greeting 0 5
hello
END
DELETED

STORED confirms the write, the VALUE ... END block is the read, and DELETED confirms the removal. If you get no output at all, Memcached is not listening on that address; recheck the -l line and the service status.

Step 4 - Allowing access from application servers (optional)

When your application runs on other servers, bind Memcached to the server's private IP address as well as localhost. Memcached has no authentication in the default Ubuntu build, so the network is your only access control: never listen on a public address.

Edit the listen line in /etc/memcached.conf, replacing your_private_ip:

sudo nano /etc/memcached.conf
-l 127.0.0.1,your_private_ip

Restart Memcached and allow only your application servers through UFW. Repeat the ufw allow command for each server, replacing app_server_ip with its private address:

sudo systemctl restart memcached
sudo ufw allow from app_server_ip to any port 11211 proto tcp

From an application server, check that it can reach the cache:

printf 'version\r\nquit\r\n' | nc your_private_ip 11211
VERSION 1.6.24

Step 5 - Caching data from Python

The typical way to use Memcached is the cache-aside pattern: look up the key; on a miss, load the data from the database, store it with a TTL and return it. Install the pymemcache client from the Ubuntu repository:

sudo apt install python3-pymemcache

Create a small demonstration script:

nano ~/memcached_demo.py

The slow_database_query function stands in for a real query that takes half a second:

import json
import time

from pymemcache.client.base import Client

cache = Client(("127.0.0.1", 11211), connect_timeout=1, timeout=0.5)


def slow_database_query(user_id):
    time.sleep(0.5)  # simulate a slow SQL query
    return {"id": user_id, "name": f"User {user_id}", "plan": "pro"}


def get_user(user_id, ttl=300):
    key = f"user:{user_id}"
    cached = cache.get(key)
    if cached is not None:
        return json.loads(cached), "hit"

    user = slow_database_query(user_id)
    cache.set(key, json.dumps(user), expire=ttl)
    return user, "miss"


def invalidate_user(user_id):
    # Call this after updating the user in the database
    cache.delete(f"user:{user_id}")


if __name__ == "__main__":
    for _ in range(3):
        start = time.perf_counter()
        user, status = get_user(7)
        print(f"{status:4} {(time.perf_counter() - start) * 1000:7.1f} ms  {user}")

    # Fetch several keys in one round trip
    print(cache.get_many(["user:7", "user:8"]))

Run it:

python3 ~/memcached_demo.py
miss   502.6 ms  {'id': 7, 'name': 'User 7', 'plan': 'pro'}
hit      0.3 ms  {'id': 7, 'name': 'User 7', 'plan': 'pro'}
hit      0.2 ms  {'id': 7, 'name': 'User 7', 'plan': 'pro'}
{'user:7': b'{"id": 7, "name": "User 7", "plan": "pro"}'}

Only the first call hits the "database". get_many returns just the keys that exist (user:8 was never stored), and it fetches them in a single network round trip, which matters when a page needs dozens of cached objects.

Keep these rules in mind when you design keys:

  • Keys are limited to 250 bytes and cannot contain spaces or control characters. Hash long identifiers (for example with SHA-256) before using them in a key.
  • Always set a TTL. Memcached evicts by LRU when memory is full, but the TTL is what keeps stale data from being served.
  • Treat every miss as normal. Memcached can be restarted or can evict any key at any time, so the application must always be able to rebuild the value.

If you run several Memcached servers, use pymemcache.client.hash.HashClient with the list of servers. The client hashes each key to one server, so the total cache size is the sum of all servers and losing one server only loses its share of the keys.

Step 6 - Storing PHP sessions in Memcached (optional)

Storing PHP sessions in Memcached lets several web servers share them. Install the PHP extension for your PHP version (8.3 on Ubuntu 24.04):

sudo apt install php8.3-memcached

Open the PHP-FPM configuration file:

sudo nano /etc/php/8.3/fpm/php.ini

Find the session.save_handler and session.save_path directives and set them as follows. With the memcached extension, the save path is a plain host:port list without a protocol prefix:

session.save_handler = memcached
session.save_path = "127.0.0.1:11211"

Restart PHP-FPM:

sudo systemctl restart php8.3-fpm

Check that the setting is active in the FPM configuration:

php-fpm8.3 -i | grep -E "^session.save_(handler|path)"
session.save_handler => memcached => memcached
session.save_path => 127.0.0.1:11211 => 127.0.0.1:11211

For WordPress, an object cache plugin with a Memcached backend uses the same php8.3-memcached extension and the same server.

Step 7 - Monitoring hit rate, evictions and connections

Memcached exposes its counters through the stats command:

printf 'stats\r\nquit\r\n' | nc 127.0.0.1 11211 | grep -E "curr_connections|listen_disabled_num|get_hits|get_misses|evictions|curr_items| bytes |limit_maxbytes"
STAT curr_connections 38
STAT listen_disabled_num 0
STAT get_hits 482113
STAT get_misses 31207
STAT curr_items 20481
STAT bytes 187342112
STAT limit_maxbytes 536870912
STAT evictions 0

How to read these values:

  • Hit rate: get_hits / (get_hits + get_misses). Here it is about 94 percent. A low hit rate means short TTLs, keys that are never reused, or a cache that is too small.
  • evictions: items removed before their TTL expired because memory was full. A steadily growing number, together with a falling hit rate, means -m is too small.
  • bytes and limit_maxbytes: current memory used by items and the configured limit.
  • listen_disabled_num: how many times Memcached stopped accepting new connections because it reached the -c limit. Any value above zero means you should raise -c.

These counters reset when the service restarts. For continuous tracking, feed them into your monitoring system, for example with the Prometheus memcached_exporter or the Memcached integration of your monitoring agent.

Troubleshooting

Clients time out or get Connection refused. Check that the service is running with systemctl status memcached, that ss -ltnp | grep 11211 shows the expected address, and that UFW allows the client's IP. Look at /var/log/memcached.log and sudo journalctl -u memcached for startup errors caused by a mistyped option.

SERVER_ERROR object too large for cache. The value is bigger than item_size_max (1 MB by default). Compress the value in the client, split it, or raise the limit with -I in /etc/memcached.conf.

High evictions even though bytes is below the limit. Memcached allocates memory in slab classes by item size. If your item sizes change a lot over time, memory may be assigned to classes you no longer use. Recent versions rebalance slabs automatically; if evictions persist, increase -m and check which item sizes dominate with printf 'stats slabs\r\nquit\r\n' | nc 127.0.0.1 11211.

Conclusion

You installed Memcached on Ubuntu 24.04, gave it a proper memory, connection and thread budget, kept it on localhost or a private network, used it from Python with the cache-aside pattern and from PHP as a session store, and learned which statistics tell you whether the cache is working.

Next, you can add more Memcached servers behind a hashing client as your cache grows, connect Memcached metrics to your monitoring stack, or compare it with Redis if you need persistence, replication or richer data types.