Linux cgroups v2 and Systemd Slices for PHP-FPM Resource Control

On a shared WordPress server running multiple PHP-FPM pools, one pool’s CPU spike or memory leak can starve the others. Linux cgroups v2 — exposed through systemd slices and service limits — lets you assign hard CPU and memory ceilings to each pool so a runaway plugin cannot take the entire server down. This requires no kernel patching: systemd 240+ on any modern Linux distribution supports it out of the box.

Problem: PHP-FPM and MySQL workers on a shared WordPress server compete for CPU and RAM — a traffic spike on one site starves another, and there is no mechanism to enforce resource limits without killing processes.

Solution: Use Linux cgroups v2 to assign each service to a systemd slice: configure CPUQuota, MemoryMax, and MemorySwapMax in a .slice unit file for PHP-FPM and MySQL separately. Systemd enforces the limits at the kernel level — processes that exceed memory limits are throttled rather than killed, and CPU quotas prevent one service from starving another.


The configuration below creates a systemd slice for PHP-FPM, sets CPU and memory limits, assigns the PHP-FPM service to the slice, and shows how to verify the limits are active with systemctl status and cat /sys/fs/cgroup/....


# /etc/systemd/system/web-workers.slice
# Create a slice to group all web-facing PHP-FPM pools
[Unit]
Description=Web Workers Slice
Before=slices.target

[Slice]
# Limit the entire slice: all PHP-FPM pools together
CPUQuota=400%          # 4 full CPU cores across all pools
MemoryHigh=3G          # soft limit — kernel reclaims aggressively above this
MemoryMax=4G           # hard limit — processes get SIGKILL if exceeded
MemorySwapMax=0        # disable swap for this slice


# /etc/systemd/system/php8.2-fpm.service.d/override.conf
# Assign PHP-FPM to the slice and set per-service limits
[Service]
Slice=web-workers.slice
CPUQuota=200%           # this pool: max 2 cores
MemoryHigh=1500M
MemoryMax=2G
TasksMax=512            # max threads/processes
# Restart automatically if OOM-killed
Restart=on-failure
RestartSec=3s


# Apply and verify
systemctl daemon-reload
systemctl restart php8.2-fpm

# Check effective cgroup limits
systemctl status php8.2-fpm
# Look for: CGroup: /web-workers.slice/php8.2-fpm.service

# Inspect raw cgroup v2 values
SERVICE_CGROUP=$(systemctl show -p ControlGroup --value php8.2-fpm)
cat /sys/fs/cgroup${SERVICE_CGROUP}/memory.max
cat /sys/fs/cgroup${SERVICE_CGROUP}/cpu.max
# cpu.max format:  quota_usec  period_usec  (e.g. "200000 100000" = 200%)

# Monitor real-time usage
systemd-cgtop -d 1    # live cgroup CPU/memory usage, 1s refresh


NOTE: Set MemoryHigh rather than MemoryMax as the primary guard — MemoryHigh triggers gradual memory reclaim and throttling while the process continues running, whereas MemoryMax causes an immediate OOM kill; use both together so the soft limit handles spikes gracefully and the hard limit is a last resort.