EST. 2026 — Notes from real server work SYSADMIN · SECURITY · AI · DIRECTADMIN Issue 01
THE SYSADMIN ALMANAC
Home / Linux / Per-user Redis on shared hosting—and the maxmemory bug that hung sites
Linux

Per-user Redis on shared hosting—and the maxmemory bug that hung sites

by Ton-Or ·9 Jul 2026·10 min read
Per-user Redis on shared hosting—and the maxmemory bug that hung sites

Letting every site share one Redis instance on shared hosting is asking for trouble — but separating instances per user and controlling them with cgroups has its own traps. Two numbers that do not talk to each other can hang an entire site in ways that are hard to diagnose.

On a shared hosting server, letting every site use the same Redis instance leads to the classic problems: one site floods it with keys and other sites suffer evictions, cached data gets mixed across tenants, and one FLUSHALL hurts the whole server. So I designed it as one Redis instance per user, with resources controlled by systemd cgroups — after running it for a while, I hit a bug that I want to cover in the second half of this article.

The usernames and tier numbers in this article are examples. The structure and config are real.

Structure: one systemd template unit for every user

# /etc/systemd/system/[email protected] (ตัดมาเฉพาะส่วนสำคัญ)
[Unit]
Description=Redis per-user (%i)

[Service]
User=%i
Group=%i
ExecStart=/usr/bin/redis-server /usr/local/directadmin/data/users/%i/redis.conf
# กำแพงทรัพยากรจริงอยู่ตรงนี้ — cgroup v2
MemoryHigh=150M
MemoryMax=200M
CPUQuota=50%

Each user's redis.conf — the key point is that there is no TCP port open at all; it uses a UNIX socket in the user's own home directory:

port 0
unixsocket /home/webuser/.redis/redis.sock
unixsocketperm 700
maxmemory 120mb
maxmemory-policy allkeys-lru
# กันยิงคำสั่งอันตรายจากฝั่งแอป
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""

That gives full isolation: each site has its own instance, socket file permissions of 700 keep others out, and PHP code cannot meddle with CONFIG SET because it has been renamed away (changing the config requires editing the file + restarting).

Then one day, a WooCommerce site hung

One customer reported that clicking to publish a product in WordPress brought up:

RedisException: socket error on read socket

It happened intermittently, not all the time. Restarting Redis fixed it briefly, then it came back — this kind of "comes and goes" behavior is usually a resource problem. Here is what was actually running:

systemctl status redis@webuser
# Memory: 149.8M (high: 150.0M max: 200.0M)  <- ชนเพดาน high ตลอดเวลา

grep maxmemory /usr/local/directadmin/data/users/webuser/redis.conf
# maxmemory 1gb    <- ต้นเหตุ

See it yet? — Redis was told it had 1GB, but the cgroup actually gave it only 150MB

How it hangs: two numbers that do not talk to each other

Redis maxmemory

Evicts on its own at the limit

Once it reaches the limit, Redis evicts keys itself according to allkeys-lru — fast, no symptoms, just cache misses for the site

cgroup MemoryHigh

The kernel hits the brakes above the limit

The process stays alive, but gets throttled + forced to reclaim memory pages. Every syscall slows down enormously — the app sees a timeout

With the conf pretending there is 1GB available, Redis does not evict anything (it has not reached its own limit). Memory usage keeps growing until it hits the cgroup's MemoryHigh → the kernel hits the brakes on the whole process → PHP cannot wait any longer for a response → that is the "socket error on read socket".

Path to a hangconf 1GBcgroup 150M→Redis does not evictbelow its own limit→Hits MemoryHighkernel throttle→PHP timeoutsocket error

The rule I arrived at: maxmemory must not exceed 80% of cgroup high

Leave 20% for Redis's own overhead (memory allocation during fork, buffers, etc.):

MemoryHigh 150M  ->  maxmemory 120mb
MemoryHigh  50M  ->  maxmemory  40mb

This way, Redis always evicts on its own before the kernel steps in — the site gets a cache miss (a little slower) instead of a socket timeout (the whole page breaks).

MemoryHigh 150McgroupNormal use up to MemoryHighthrottleCorrectmaxmemory 120mbSpare 20%Redis evicts before kernel intervention; just a cache missWrongmaxmemory 1gb (beyond chart)No eviction before MemoryHigh; throttled, PHP times outMemoryMax 200M
All three rows use the same scale. The app's internal setting must stay below the cgroup boundary, at no more than 80 percent of MemoryHigh, or the app will never know it is being squeezed.

Preventing recurrence with a guard script

Fixing one server is not enough. I need to prevent this across the system — cron runs every 15 minutes, checking every instance:

#!/bin/bash
# redis-tier-guard: บังคับ conf maxmemory <= 80% cgroup + restart ตัวที่กำลังโดน throttle
for u in $(systemctl list-units 'redis@*' --state=running --no-legend | sed 's/redis@\([^.]*\).*/\1/'); do
  CONF=/usr/local/directadmin/data/users/$u/redis.conf
  HIGH=$(systemctl show redis@$u -p MemoryHigh --value)         # bytes
  [ "$HIGH" = "infinity" ] && continue
  CAP=$((HIGH * 80 / 100 / 1048576))                             # MB ที่อนุญาต
  CUR=$(grep -oP '^maxmemory \K[0-9]+' <<< "$(grep -i '^maxmemory ' $CONF | sed 's/mb//i')")
  if [ "${CUR:-0}" -gt "$CAP" ]; then
    sed -i "s/^maxmemory .*/maxmemory ${CAP}mb/i" $CONF
    systemctl restart redis@$u
    echo "[FIX] $u: maxmemory ${CUR}mb -> ${CAP}mb (cgroup high $((HIGH/1048576))M)"
    # ตรงนี้ต่อ alert เข้า Telegram/อีเมลของทีมได้
  fi
  # ตัวที่ใช้จริงเกิน 90% ของ high = กำลังจะโดน throttle -> restart ระบาย
  USED=$(systemctl show redis@$u -p MemoryCurrent --value)
  [ "$USED" != "" ] && [ "$USED" -gt $((HIGH * 90 / 100)) ] && systemctl restart redis@$u
done
# /etc/cron.d/redis-tier-guard
*/15 * * * * root /usr/local/sbin/redis-tier-guard.sh >> /var/log/redis-tier-guard.log 2>&1

One decision I spent a long time considering was "why not just alert and let someone fix it?" — the answer is that leaving this alone means a customer's site hangs in the middle of the night, while an auto-fix means restarting the cache once (losing a little cached data). Compared side by side, the auto-fix costs far less. Whenever an instance drifts outside its tier, the system needs to bring itself back into working order.

Do not put queues in Redis configured this way

allkeys-lru means every key can be evicted — losing cache is fine, but losing a queued job means losing work. If an app (such as Laravel) needs a queue, use a database driver or a separate Redis instance configured with noeviction only. I learned this the hard way.

Personally, I consider this 80% rule applicable to every service running under a cgroup, not just Redis — the number inside the app must always stay below the boundary outside it, or the app will never know it is being squeezed. If you want to use this setup, adjust the tier numbers to suit your server. If I have missed anything in the explanation or made a typo, I will come back and correct it later.

Want us to set this up?

Get help with server setup, configuration, migration, or troubleshooting. Save time for the work that matters.

Get a quote

Hit something like this? Stuck somewhere?

Come trade notes on the LinuxThai board

→ Continue on the board

Comments 0

No comments yet — be the first