Use Redis as a cache: TTLs, maxmemory and an eviction health report
Set a memory limit and an eviction policy so Redis behaves like a cache, give keys TTLs, then run a bash report on memory use, hit ratio, evictions, persistence and keyspace.
A cache holds copies of data whose real home is somewhere slower, such as a SQL database or an API. When memory runs out, it is fine to throw cache entries away, because they can be fetched again. Redis does this for you once you tell it how much memory it may use and which keys it may evict.
1. Cache-aside with a TTL
The most common pattern: the application reads from Redis first; on a miss it loads the data from the database and stores it in Redis with an expiry.
GET product:42 # (nil) = cache miss
SET product:42 "{...json...}" EX 300 # store for 5 minutes after loading from the DB
TTL product:42
Every cache entry should have a TTL, so stale data disappears on its own. When the source data changes, delete the key (DEL product:42) so the next read loads fresh data.
2. Set maxmemory and a policy
On 64-bit systems maxmemory is 0 by default, which means no limit: Redis keeps growing until the operating system runs out of memory. Set a limit in redis.conf (often /etc/redis/redis.conf on Linux packages):
maxmemory 2gb
maxmemory-policy allkeys-lru
Or change it live and save the running configuration back to the file:
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli CONFIG REWRITE
The default policy is noeviction: when the limit is reached, commands that add data fail with OOM command not allowed when used memory > 'maxmemory'. while reads keep working. That is right for data you must not lose, wrong for a cache. The main policies:
allkeys-lru: evict the least recently used keys. A good default when some keys are much hotter than others.allkeys-lfu: evict the least frequently used keys (Redis 4.0 and later).allkeys-random: evict random keys, for access patterns where every key is equally likely.volatile-lru,volatile-lfu,volatile-random,volatile-ttl: the same ideas, but only keys with an expiry are candidates. If no key has a TTL, they behave likenoeviction.- Redis 8.6 added
allkeys-lrmandvolatile-lrm, which evict the least recently modified keys.
If the same instance also holds data that must never be evicted, use a volatile-* policy and give TTLs only to cache keys, or better, run two separate instances.
Leave headroom below the machine’s RAM. With replication or AOF persistence, Redis uses extra buffers that do not count toward maxmemory; INFO memory shows them as mem_not_counted_for_evict. Replicas ignore maxmemory by default and rely on the primary’s evictions.
3. Measure: the report script
Eviction works best when you watch it. The script below reads INFO once and prints memory use against the limit, the hit ratio keyspace_hits / (keyspace_hits + keyspace_misses), evicted and expired keys, the persistence status and per-database key counts. With -s it also runs redis-cli --memkeys, which walks the keyspace with SCAN (throttled with -i 0.01) to show the largest keys. It only reads; it never changes data or configuration.
#!/usr/bin/env bash
# redis-report.sh: memory, eviction, persistence and keyspace report. License: CC0-1.0
set -euo pipefail
HOST=127.0.0.1
PORT=6379
USER_NAME=""
USE_TLS=no
SCAN_KEYS=no
WARN_PCT=90
usage() {
cat <<USAGE
Usage: $0 [-h host] [-p port] [-u acl-user] [-t] [-s] [-w percent]
-t connect with TLS
-s also scan the keyspace for the biggest keys (throttled, read-only)
-w warn when used memory exceeds this percent of maxmemory (default $WARN_PCT)
Password: export REDISCLI_AUTH=... before running (never put it on the command line).
USAGE
}
while getopts ':h:p:u:tsw:' opt; do
case "$opt" in
h) HOST=$OPTARG ;;
p) PORT=$OPTARG ;;
u) USER_NAME=$OPTARG ;;
t) USE_TLS=yes ;;
s) SCAN_KEYS=yes ;;
w) WARN_PCT=$OPTARG ;;
*) usage; exit 2 ;;
esac
done
command -v redis-cli >/dev/null 2>&1 || { echo "redis-cli not found in PATH" >&2; exit 1; }
cli=(redis-cli -h "$HOST" -p "$PORT" -e --no-auth-warning)
if [[ -n "$USER_NAME" ]]; then
cli+=(--user "$USER_NAME")
fi
if [[ "$USE_TLS" == yes ]]; then
cli+=(--tls)
fi
if ! info=$("${cli[@]}" INFO 2>&1); then
echo "INFO failed: $info" >&2
exit 1
fi
info=$(printf '%s\n' "$info" | tr -d '\r')
get() {
printf '%s\n' "$info" | awk -v k="$1" 'index($0, k ":") == 1 { print substr($0, length(k) + 2); found = 1; exit } END { if (!found) print "n/a" }'
}
warnings=()
used=$(get used_memory)
max=$(get maxmemory)
hits=$(get keyspace_hits)
misses=$(get keyspace_misses)
evicted=$(get evicted_keys)
policy=$(get maxmemory_policy)
bgsave=$(get rdb_last_bgsave_status)
aof_on=$(get aof_enabled)
aof_write=$(get aof_last_write_status)
echo "== Redis report for $HOST:$PORT at $(date '+%Y-%m-%d %H:%M:%S')"
echo
echo "-- Server"
printf ' %-28s %s\n' "version" "$(get redis_version)" \
"role" "$(get role)" \
"uptime (days)" "$(get uptime_in_days)" \
"connected clients" "$(get connected_clients)" \
"connected replicas" "$(get connected_slaves)"
echo
echo "-- Memory"
printf ' %-28s %s\n' "used" "$(get used_memory_human)" \
"peak" "$(get used_memory_peak_human)" \
"maxmemory" "$(get maxmemory_human)" \
"maxmemory-policy" "$policy" \
"fragmentation ratio" "$(get mem_fragmentation_ratio)"
if [[ "$max" =~ ^[0-9]+$ && "$used" =~ ^[0-9]+$ ]]; then
if (( max == 0 )); then
warnings+=("maxmemory is 0 (no limit): a cache can grow until the OS runs out of memory")
else
pct=$(( used * 100 / max ))
printf ' %-28s %s%%\n' "used / maxmemory" "$pct"
if (( pct >= WARN_PCT )); then
warnings+=("memory use is at ${pct}% of maxmemory")
fi
fi
fi
echo
echo "-- Cache efficiency"
ratio=$(awk -v h="$hits" -v m="$misses" 'BEGIN { if (h + m > 0) printf "%.1f%%", h * 100 / (h + m); else print "no lookups yet" }')
printf ' %-28s %s\n' "keyspace hits" "$hits" \
"keyspace misses" "$misses" \
"hit ratio" "$ratio" \
"evicted keys" "$evicted" \
"expired keys" "$(get expired_keys)"
if [[ "$evicted" =~ ^[0-9]+$ ]] && (( evicted > 0 )); then
warnings+=("$evicted keys were evicted since the last restart or CONFIG RESETSTAT")
fi
echo
echo "-- Persistence"
printf ' %-28s %s\n' "last RDB save status" "$bgsave" \
"changes since last save" "$(get rdb_changes_since_last_save)" \
"AOF enabled" "$aof_on" \
"last AOF write status" "$aof_write"
if [[ "$bgsave" == "err" ]]; then
warnings+=("the last RDB snapshot failed, check the Redis log (writes may be blocked with MISCONF)")
fi
if [[ "$aof_on" == "1" && "$aof_write" == "err" ]]; then
warnings+=("the last AOF write failed, check disk space and the Redis log")
fi
echo
echo "-- Keyspace"
keyspace=$(printf '%s\n' "$info" | grep -E '^db[0-9]+:' || true)
if [[ -n "$keyspace" ]]; then
printf '%s\n' "$keyspace" | sed 's/^/ /'
else
echo " (no keys)"
fi
if [[ "$SCAN_KEYS" == yes ]]; then
echo
echo "-- Biggest keys by memory (SCAN, throttled)"
"${cli[@]}" --memkeys -i 0.01 | sed 's/^/ /'
fi
echo
if (( ${#warnings[@]} == 0 )); then
echo "No warnings."
else
echo "Warnings:"
printf ' - %s\n' "${warnings[@]}"
exit 3
fi
Run it
chmod +x redis-report.sh
./redis-report.sh # local, default user, no password
export REDISCLI_AUTH='CHANGE_ME' # password via environment
./redis-report.sh -h cache1.example.com -u monitor
./redis-report.sh -h cache1.example.com -u monitor -t -s
The password goes in REDISCLI_AUTH, never on the command line, where ps and shell history would expose it. A monitoring user only needs the INFO command (plus SCAN and MEMORY for -s), for example ACL SETUSER monitor on >CHANGE_ME +info +ping. The script exits with status 3 when it prints warnings, so cron or a monitoring agent can alert on it.
Reading the numbers
- Low hit ratio and many evicted keys: the cache is too small for its hot data, or the policy evicts the wrong keys. Raise
maxmemoryor tryallkeys-lruorallkeys-lfu. - Low hit ratio, few evictions, many expired keys: TTLs may be too short, so entries vanish before they are reused.
- maxmemory 0: no limit is set. Fine for a primary data store with enough RAM, risky for a cache.
- Last RDB save status err: snapshots are failing; see the Redis MISCONF fix.
The counters are totals since the server started or since statistics were last reset, so compare two runs some minutes apart to see current rates.
Official documentation
See also
- 0236 Redis data types and commands: a quick reference Redis stores values under keys, and each value has a type: string, hash, list, set, sorted set or stream. The essential commands… Beginner · Reference · Any system Article Basics 15 Oct 2026
- 0238 Redis persistence and high availability: RDB, AOF, replicas and Sentinel Redis keeps data in memory and writes it to disk with RDB snapshots, an append-only file, or both. Replicas copy data to… Intermediate · Concept · Linux Article Administration & persistence 15 Oct 2026
- 0239 Fix: (error) NOAUTH Authentication required. (and securing Redis with ACLs) Redis requires a login and your client sent a command without one. Pass the right user and password, and use the moment… Advanced · Fix · Linux Article Administration & persistence · Security & access 15 Oct 2026
- 0240 Fix: MISCONF Redis is configured to save RDB snapshots, but it’s currently unable to persist to disk Redis blocks writes because its last background snapshot failed. Find the cause in the log (full disk, permissions, failed fork), fix it,… Intermediate · Fix · Linux Article Administration & persistence 15 Oct 2026
- 0259 Speed up a slow database query with a Redis cache: cache-aside in Python, measured and locked down Put Redis in front of a slow query in a small Python app: cache-aside reads, TTLs with jitter, invalidation on writes, an… Intermediate · Tutorial · Linux Article Administration & persistence · Basics 15 Oct 2026
Repos in the wild
- Redis server source code (redis/redis) redis/redis The official repository of Redis Open Source: the in-memory data structure server, redis-cli, Sentinel and the example redis.conf. GitHub
- Valkey: open source key-value datastore (valkey-io/valkey) valkey-io/valkey Valkey is a BSD-licensed key-value data structure server, forked from Redis just before its 2024 license change and backed by the Linux Foundation. GitHub
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- commitpedia-admin +9,609 chars