Skip to content
Commitpedia
Log in Join & edit

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.

Not checked by a trusted editor yet Last edited Outdated?

Edit history of Use Redis as a cache: TTLs, maxmemory and an eviction health report: 1 recorded edit
Edit history · 1 edit
Type
Script
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Language
bash
Licence
CC0-1.0

What this changes

Read-only: the report runs INFO (and with -s a throttled key scan) and prints the results; no key or setting is changed. The separate CONFIG SET and CONFIG REWRITE commands change maxmemory and rewrite redis.conf.

How to undo it

Nothing to undo for the report. To revert the CONFIG examples, set the old values back, e.g. redis-cli CONFIG SET maxmemory 0, then CONFIG REWRITE. Copy redis.conf before rewriting it.

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 like noeviction.
  • Redis 8.6 added allkeys-lrm and volatile-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 maxmemory or try allkeys-lru or allkeys-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

Repos in the wild

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Commitpedia +9,609 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?