Redis is an in-memory key-value store that answers most requests in well under a millisecond, which makes it a common choice for caching database queries, API responses and rendered fragments. In this tutorial you will install Redis on Ubuntu 24.04, configure it as a pure cache with a memory limit and an eviction policy, protect it with a password, use it from a small Python application with the cache-aside pattern, and check how well the cache is working.
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
sudoprivileges. - Python 3 (installed by default on Ubuntu 24.04) for the example application in Step 5.
Step 1 - Installing Redis
Ubuntu 24.04 ships Redis 7.0 in its main repository, which is maintained with security updates for the life of the release. Install the server:
sudo apt update
sudo apt install redis-server
The package starts Redis as the redis-server systemd service and enables it at boot. Check that it is running:
sudo systemctl status redis-server --no-pager
● redis-server.service - Advanced key-value store
Loaded: loaded (/usr/lib/systemd/system/redis-server.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-09-25 10:12:03 UTC; 8s ago
...
Send a PING with the command-line client:
redis-cli ping
PONG
Confirm that Redis only listens on the loopback interface. This is the package default and it is what you want unless another server needs to reach it:
sudo ss -ltnp | grep 6379
LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=2841,fd=6))
LISTEN 0 511 [::1]:6379 [::]:* users:(("redis-server",pid=2841,fd=7))
Step 2 - Configuring Redis as a cache
By default Redis behaves like a database: it has no memory limit and it periodically saves its dataset to disk. For a cache you want the opposite: a fixed memory budget, automatic eviction of old keys when that budget is full, and no disk persistence, because cached data can always be regenerated from the source.
Open the main configuration file:
sudo nano /etc/redis/redis.conf
Find each of the following directives (use Ctrl+W to search) and set them to these values. If a directive is commented out with #, remove the #:
# Memory budget for cached data
maxmemory 256mb
# When the budget is full, evict the least recently used keys
maxmemory-policy allkeys-lru
# Pure cache: no RDB snapshots and no append-only file
save ""
appendonly no
Choose maxmemory according to your server. A reasonable starting point is 25 to 50 percent of the RAM that is not used by your application and database. Redis needs some memory on top of maxmemory for its own overhead and client buffers, so never set it to the full RAM of the server.
The eviction policy decides which keys are removed when the limit is reached:
| Policy | Evicts | When to use it |
|---|---|---|
noeviction | Nothing, writes return an error | Redis used as a database or queue (the default) |
allkeys-lru | Least recently used keys | General-purpose cache (recommended) |
allkeys-lfu | Least frequently used keys | Caches with a small set of very popular keys |
volatile-lru | Least recently used keys that have a TTL | Mixed use: cache keys with TTL next to permanent keys |
volatile-ttl | Keys closest to expiring | When TTLs reflect how valuable each key is |
If the same Redis instance also stores data you must not lose (for example job queues), do not use an allkeys-* policy. Run a separate Redis instance for caching instead.
Step 3 - Requiring a password
Even when Redis only listens on localhost, any local process or compromised web application can talk to it. Set a password with the requirepass directive. Generate a long random value first:
openssl rand -base64 32
Xq3m9Wc4oF2m8JdN1hTq0bR7pLkVs6Ey5uZaCgHiYw0=
Open the configuration file again:
sudo nano /etc/redis/redis.conf
Find the commented # requirepass foobared line and replace it with your own value:
requirepass your_redis_password
Replace your_redis_password with the string you generated. The file is readable only by the redis user and root, so the password is not exposed to other users. Restart Redis to apply all the changes from Steps 2 and 3:
sudo systemctl restart redis-server
An unauthenticated command is now rejected:
redis-cli ping
(error) NOAUTH Authentication required.
Use --askpass so the password is not stored in your shell history, then confirm the cache settings:
redis-cli --askpass CONFIG GET 'maxmemory*'
Please input password: ********
1) "maxmemory"
2) "268435456"
3) "maxmemory-policy"
4) "allkeys-lru"
5) "maxmemory-samples"
6) "5"
7) "maxmemory-eviction-tenacity"
8) "10"
268435456 bytes is 256 MB, so the limit and the policy are active.
Step 4 - Working with keys and expiration times
Open an interactive session and authenticate:
redis-cli --askpass
Store a value with a time to live (TTL) of 300 seconds using the EX option, read it back and check how long it has left:
127.0.0.1:6379> SET cache:product:42 '{"id":42,"name":"Widget","price":19.9}' EX 300
OK
127.0.0.1:6379> GET cache:product:42
"{\"id\":42,\"name\":\"Widget\",\"price\":19.9}"
127.0.0.1:6379> TTL cache:product:42
(integer) 295
When the TTL reaches zero Redis deletes the key automatically. Delete a key explicitly when the underlying data changes:
127.0.0.1:6379> DEL cache:product:42
(integer) 1
127.0.0.1:6379> GET cache:product:42
(nil)
127.0.0.1:6379> exit
Two habits keep a cache manageable:
- Always set a TTL on cached keys. Even with an eviction policy, TTLs keep stale data from living forever.
- Use structured key names such as
cache:product:42orcache:api:users:page:1. A consistent prefix makes it easy to find related keys and to invalidate them.
Step 5 - Caching database queries from Python
The most common way to use Redis as a cache is the cache-aside pattern: the application first looks for the value in Redis; on a miss it loads the value from the database, stores it in Redis with a TTL, and returns it. The next request for the same key is served from memory.
Install the Python client from the Ubuntu repository:
sudo apt install python3-redis
Create a small example script:
nano ~/cache_demo.py
Add the following code. The slow_database_query function stands in for a real database call that takes half a second:
import json
import os
import time
import redis
r = redis.Redis(
host="127.0.0.1",
port=6379,
password=os.environ["REDIS_PASSWORD"],
decode_responses=True,
)
def slow_database_query(product_id):
time.sleep(0.5) # simulate a slow SQL query
return {"id": product_id, "name": f"Product {product_id}", "price": 19.9}
def get_product(product_id, ttl=300):
key = f"cache:product:{product_id}"
cached = r.get(key)
if cached is not None:
return json.loads(cached), "hit"
product = slow_database_query(product_id)
r.set(key, json.dumps(product), ex=ttl)
return product, "miss"
def update_product(product_id, data):
# Write to the database first, then invalidate the cached copy
r.delete(f"cache:product:{product_id}")
if __name__ == "__main__":
for attempt in range(3):
start = time.perf_counter()
product, status = get_product(42)
elapsed_ms = (time.perf_counter() - start) * 1000
print(f"{status:4} {elapsed_ms:7.1f} ms {product}")
Pass the password through an environment variable instead of writing it into the code. read -s prompts for it without echoing it to the screen:
read -s -p "Redis password: " REDIS_PASSWORD && export REDIS_PASSWORD
python3 ~/cache_demo.py
miss 503.8 ms {'id': 42, 'name': 'Product 42', 'price': 19.9}
hit 0.4 ms {'id': 42, 'name': 'Product 42', 'price': 19.9}
hit 0.3 ms {'id': 42, 'name': 'Product 42', 'price': 19.9}
The first call pays the full cost of the query; the following calls are answered from Redis in a fraction of a millisecond until the TTL expires or update_product deletes the key. In a real application, load the password from your usual secrets mechanism (for example an environment file readable only by the service user) and create the redis.Redis client once at startup, because it keeps a connection pool that is reused across requests.
Most frameworks have Redis cache backends built in, such as Django's django.core.cache.backends.redis.RedisCache, Laravel's redis cache driver or Rails' :redis_cache_store. They implement the same pattern for you; you only need to point them at 127.0.0.1:6379 with the password.
Step 6 - Monitoring the cache
A cache is only useful if most lookups are hits. Redis counts hits and misses in the stats section of INFO:
redis-cli --askpass INFO stats | grep -E "keyspace_(hits|misses)|evicted_keys|expired_keys"
Please input password: ********
expired_keys:118
evicted_keys:0
keyspace_hits:15732
keyspace_misses:1204
The hit rate is keyspace_hits / (keyspace_hits + keyspace_misses), here about 93 percent. A low hit rate usually means TTLs are too short or keys are not reused. A steadily growing evicted_keys means the cache is full and Redis is removing keys before they expire; if that hurts the hit rate, increase maxmemory.
Check memory usage against the limit:
redis-cli --askpass INFO memory | grep -E "used_memory_human|maxmemory_human|mem_fragmentation_ratio"
Please input password: ********
used_memory_human:41.87M
maxmemory_human:256.00M
mem_fragmentation_ratio:1.12
Other useful checks:
redis-cli --askpass --statprints a live, one-line-per-second summary of keys, memory, clients and requests.redis-cli --askpass --bigkeysscans the keyspace (without blocking Redis) and reports the largest keys of each type.SLOWLOG GET 10insideredis-clilists the ten most recent slow commands.
Avoid the KEYS command on a production instance: it scans every key in one blocking operation. Use SCAN with a pattern, for example SCAN 0 MATCH cache:product:* COUNT 100, when you need to find keys.
Step 7 - Allowing access from another server (optional)
If your application runs on a different server, bind Redis to a private network address rather than the public one. Open the configuration file:
sudo nano /etc/redis/redis.conf
Change the bind line to include the private IP of the Redis server, replacing your_private_ip:
bind 127.0.0.1 -::1 your_private_ip
Restart Redis and allow only the application server through UFW, replacing app_server_ip with its private address:
sudo systemctl restart redis-server
sudo ufw allow from app_server_ip to any port 6379 proto tcp
From the application server, test the connection (install redis-tools there to get redis-cli):
redis-cli -h your_private_ip --askpass ping
Please input password: ********
PONG
WarningNever expose port 6379 to the internet. Redis traffic is not encrypted by default, and exposed instances are scanned and attacked constantly. For traffic between data centers, use a private network or a VPN such as WireGuard.
Troubleshooting
OOM command not allowed when used memory > 'maxmemory'. The eviction policy is still noeviction, or you use a volatile-* policy and there are no keys with a TTL to evict. Set maxmemory-policy allkeys-lru and restart Redis.
WARNING Memory overcommit must be enabled! in the log. Redis logs this at startup. It matters mostly when persistence is enabled, because saving forks the process. To follow the recommendation, create /etc/sysctl.d/99-redis.conf with the line vm.overcommit_memory = 1 and apply it with sudo sysctl --system.
The service does not start after editing the configuration. A typo in redis.conf stops Redis at startup. Read the exact error with sudo journalctl -u redis-server -n 30 and fix the line it points to.
Clients get Connection refused from another server. Check that bind includes the private IP, that ss -ltnp | grep 6379 shows Redis listening on it, and that the UFW rule allows the client's address.
Conclusion
You installed Redis on Ubuntu 24.04, configured it as a memory-bounded LRU cache without persistence, protected it with a password, cached a slow query from Python with the cache-aside pattern and learned how to read the hit rate and eviction counters.
From here you can plug Redis into your framework's cache backend, store user sessions in it so they are shared across several application servers, or add Redis metrics such as hit rate, memory and evictions to your monitoring system.
