In short: For the PHP2Shell interim measure memory_limit=-1, PHP-FPM needs a memory limit outside PHP. Under systemd you set it with MemoryHigh= as a brake and MemoryMax= as a hard ceiling in a supplementary file for the FPM service. According to php.net, ulimit, LimitAS= or LimitDATA= are unsuitable for this. Also check OOMPolicy= so that a single worker does not terminate the whole service.
A recommendation that is surprising at first
Among the interim measures that php.net published for PHP2Shell on 21 October 2026 is one that made many administrators pause: memory_limit=-1, that is, no memory limit in PHP itself. For years the opposite was considered good practice.
A connection with the description of the vulnerability is obvious. With CVE-2026-109618, an out-of-memory condition can interrupt PHP while request data is only partially initialized. The interim measure moves the memory limit out of PHP into the operating system. php.net expressly ties it to two conditions: the value must be locked against overriding, and the limit must come via container or cgroup, not via RLIMIT_AS or RLIMIT_DATA.
Anyone who implements only the first half has a server without any memory limit. This article shows the second half. Even together, the measure remains an interim solution: the vulnerability is fixed only with PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35 or a confirmed backport.
cgroups explained in two sentences
Control groups, cgroups for short, are a Linux kernel feature that groups processes and limits their resources. systemd automatically creates a cgroup of its own for every service, and all processes of the service, in the case of the FPM service the master and all workers, belong to it.
systemd's memory options set values of the cgroup v2 memory controller. A short command shows whether your system uses cgroup v2:
# "cgroup2fs" means: cgroup v2 is active
stat -fc %T /sys/fs/cgroup/
Current versions of the common distributions use cgroup v2 by default. If the command shows something else, clarify this before the changeover, because the systemd documentation describes the old v1 options as deprecated.
Setting systemd MemoryMax for PHP-FPM
The limit goes into a supplementary file, a so-called drop-in. That way the package's unit file stays unchanged, and an update does not overwrite your setting. Service names differ by distribution; the example uses Debian and Ubuntu with PHP 8.3.
# Opens an editor for the drop-in: sudo systemctl edit php8.3-fpm
# Result: /etc/systemd/system/php8.3-fpm.service.d/override.conf
[Service]
MemoryAccounting=yes
MemoryHigh=1536M
MemoryMax=2G
MemorySwapMax=0
OOMPolicy=continue
The options at a glance, according to the systemd documentation:
| Option | Effect | Note for PHP-FPM |
|---|---|---|
MemoryHigh= |
throttling limit: processes are strongly slowed down, memory is reclaimed | the actual control value, slightly below MemoryMax |
MemoryMax= |
absolute limit: if it cannot be held, the OOM killer becomes active in the service | last line of defense |
MemorySwapMax= |
limit for swapping | 0 prevents the service from evading into swap instead |
OOMPolicy= |
behavior after an OOM event: continue, stop or kill |
continue lets the remaining workers keep running |
LimitAS=, LimitDATA= |
rlimits per process | unsuitable for this measure according to php.net |
OOMPolicy= deserves special attention. With stop or kill, systemd terminates the entire service after an OOM event, and all websites of this FPM service go down. Which value applies without an explicit setting is determined by the system default DefaultOOMPolicy=. So check the effective value instead of assuming it.
Then restart the service and lock memory_limit in the pool:
sudo systemctl restart php8.3-fpm
systemctl show php8.3-fpm -p MemoryHigh -p MemoryMax -p MemorySwapMax -p OOMPolicy -p MemoryCurrent
; Pool file, e.g. /etc/php/8.3/fpm/pool.d/seite1.conf
php_admin_value[memory_limit] = -1
How locking works is described in php_admin_value: locking settings against overrides.
Finding the right value
A limit that is too tight causes aborts in normal operation, one that is too generous hardly protects. The starting point is actual consumption.
- Observe the current memory needs of all FPM processes over a few days, including load peaks.
- Determine the typical need of a worker and multiply it by
pm.max_childrenof all pools of the service. - Allow a margin for the master process, OPcache and peaks.
- Set
MemoryHigh=just above the measured peak,MemoryMax=with some distance above that. - Readjust the values after a week based on the measurements and OOM events.
# Memory of all PHP-FPM processes in MB (resident set size)
ps -C php-fpm8.3 -o rss= | awk '{s+=$1} END {printf "%.0f MB\n", s/1024}'
# Live overview of all cgroups sorted by memory
systemd-cgtop -m
Note down the value of memory_limit before the changeover. After the update you can return to a fixed limit in PHP; how to choose it sensibly is covered under Setting memory_limit correctly.
Monitoring: when the limit takes effect
A memory limit whose triggering nobody notices is a silent source of errors. The kernel counts OOM events per cgroup, and systemd and the kernel log record them.
# OOM counter of the service's cgroup (path for cgroup v2)
cat /sys/fs/cgroup/system.slice/php8.3-fpm.service/memory.events
# OOM killer messages in the kernel log
journalctl -k --since "-1 day" | grep -i "out of memory"
If the oom_kill counter rises, the kernel is terminating workers. This can point to a limit that is too tight, a faulty application or unusual requests. In the current situation it is worth comparing such events with the access logs from the same time.
The memory limit is an interim measure and does not replace the PHP update. Plan the update in parallel, and restart all PHP processes afterwards. An overview of all interim measures is under Immediate measures if you cannot patch yet.
What happens inside PHP when memory runs out is explained in memory_limit and out-of-memory. With Apache and mod_php, the same technique applies to the Apache service, although PHP then shares the limit with the web server itself.
Conclusion: don't forget the second half
memory_limit=-1 is a defensible interim measure only together with an outer limit. systemd makes this limit possible with a few lines: MemoryHigh as a brake, MemoryMax as a hard ceiling, MemorySwapMax against evading into swap and a deliberately chosen OOMPolicy. Measure beforehand, observe afterwards, and keep in mind that all pools of a service share the limit.
Above all, though, plan the end of the measure. After the update to a fixed PHP version, return to a fixed memory_limit. You can keep the cgroup limit as additional protection against runaway processes.
How does PHP SOS help?
- Measure: We record the memory needs of your PHP services and derive suitable limits from them.
- Implement: We set drop-ins with
MemoryHigh,MemoryMaxand a suitableOOMPolicy, lockmemory_limitin the pool and restart the services in a controlled way. - Observe: We integrate OOM events into monitoring and readjust the values.
- Lead to the patch: Through the PHP patch service we apply the update and then take the interim values back in an orderly way.
Frequently asked questions
Why should memory_limit be -1? Isn't that dangerous?
On its own it would be. php.net recommends memory_limit=-1 as an interim measure expressly together with a memory limit via container or cgroup. The limitation thus moves out of PHP into the operating system. Without this outer limit you should not set the value.
Can I use ulimit or LimitAS= instead?
No. php.net expressly advises against RLIMIT_AS and RLIMIT_DATA, and these are exactly the limits set by ulimit -v, LimitAS= and LimitDATA=. The systemd documentation also points out that LimitAS= limits the address space, not memory consumption.
What happens when PHP-FPM reaches the limit?
With MemoryHigh, the kernel throttles the processes and reclaims memory. With MemoryMax, the OOM killer becomes active within the service and terminates a process, usually a large worker. Whether the whole service stops afterwards is governed by OOMPolicy=.
Does the limit apply to each pool individually?
No. All pools of an FPM service share one cgroup and thus a common limit. Separate limits per website require separate FPM services or containers.
How do I set the limit in Docker or Kubernetes?
Through the container's memory limit, for example the --memory option of docker run or resources.limits.memory in Kubernetes. Both also use cgroups. The principle stays the same: limit outside PHP, memory_limit locked in PHP.