Calcular bien pm.max_children en PHP-FPM en lugar de adivinar
El mensaje server reached pm.max_children no significa que tengas que duplicar el valor. Mide, calcula, elige el modo de funcionamiento y demuestra después que el valor encaja de verdad.
En algún momento aparece esta línea en el log de PHP-FPM, y viene con el consejo incluido:
WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
El consejo no es incorrecto, solo está incompleto. pm.max_children es el único parámetro de PHP-FPM en el que un valor demasiado alto resulta más peligroso que uno demasiado bajo. Demasiado bajo cuesta tiempo de espera y, en el peor de los casos, un 502. Demasiado alto se lleva por delante la RAM de todo el servidor, y entonces es el kernel quien decide qué proceso termina. Por experiencia, no elige PHP, sino la base de datos.
Esta guía muestra el camino que va de adivinar a calcular: medir cuánta memoria consume un proceso de trabajo, determinar el presupuesto, elegir el modo de funcionamiento y demostrar después que el valor es correcto.
Todos los datos se refieren a Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS y Ubuntu 22.04 LTS. Los comandos están escritos para ejecutarse como root; si trabajas como usuario normal, antepón sudo. Sustituye en todas partes el número de versión de PHP por el de tu sistema.
| Sistema | PHP | Servicio | Archivo del pool | Log de FPM |
| Debian 13 (trixie) | 8.4 | php8.4-fpm | /etc/php/8.4/fpm/pool.d/www.conf | /var/log/php8.4-fpm.log |
| Debian 12 (bookworm) | 8.2 | php8.2-fpm | /etc/php/8.2/fpm/pool.d/www.conf | /var/log/php8.2-fpm.log |
| Ubuntu 24.04 LTS | 8.3 | php8.3-fpm | /etc/php/8.3/fpm/pool.d/www.conf | /var/log/php8.3-fpm.log |
| Ubuntu 22.04 LTS | 8.1 | php8.1-fpm | /etc/php/8.1/fpm/pool.d/www.conf | /var/log/php8.1-fpm.log |
Un vistazo al directorio te dice qué versiones hay instaladas. En servidores que han pasado por una actualización de distribución suele haber dos:
ls /etc/php/
ls /etc/php/*/fpm/pool.d/
Qué limita realmente pm.max_children
El valor no limita las visitas ni las conexiones, sino el número de peticiones PHP que se ejecutan en el mismo instante. Una petición de 80 milisegundos ocupa un proceso de trabajo durante 80 milisegundos y después lo libera. De ahí se deduce casi todo lo demás:
- Mucho tráfico necesita pocos procesos mientras los scripts sean rápidos. 40 peticiones por segundo de 80 milisegundos cada una dan algo más de tres peticiones simultáneas, no 40.
- Un solo punto lento tumba el cálculo. Una llamada a una API ajena sin un timeout propio mantiene un proceso ocupado cinco segundos, aunque este no esté haciendo nada. Cinco llamadas así por segundo bloquean 25 procesos de forma permanente.
- Las conexiones en espera no ocupan ningún proceso. Una conexión keep-alive con nginx cuesta un descriptor de archivo, pero ningún proceso de trabajo.
Cuando todos los procesos están ocupados, las peticiones nuevas esperan en la cola de aceptación del socket. Su tamaño está en el archivo del pool bajo listen.backlog, 511 de fábrica, y el kernel la limita además con net.core.somaxconn, que en los cuatro sistemas vale 4096:
sysctl net.core.somaxconn
Mientras la cola dé abasto, el visitante solo nota tiempos de carga más largos. Cuando también se llena, el kernel rechaza la conexión, nginx registra 11: Resource temporarily unavailable y al visitante le llega un 502. Para distinguirlo del resto de causas: solucionar el error nginx 502 Bad Gateway.
Un detalle que genera mucha confusión: el aviso aparece una vez por fase de saturación, no una vez por cada petición afectada. FPM activa internamente un indicador y solo lo borra cuando vuelve a haber un proceso libre. Una sola línea puede representar toda una hora punta. Quien cuenta líneas subestima el problema. La medida honesta es el contador max children reached de la página de estado, que se consulta más abajo.
La vuelta atrás, antes de tocar nada
Aquí hay dos cosas que pueden salir mal, y las dos afectan al servicio en producción.
Primera: FPM ya no arranca. Un systemctl reload envía la señal USR2 al proceso principal, que se reinicia a sí mismo con la nueva configuración. Si esa configuración no es válida, la inicialización se interrumpe y el proceso principal termina. Una instancia en marcha se convierte así en una instancia muerta. Por eso, sin excepciones: primero comprobar, después recargar:
php-fpm8.2 -t
Si todo va bien, la salida termina con test is successful.
Segunda: el valor es demasiado alto. El servidor no se rompe al instante, sino en el siguiente pico de carga, y lo hace de forma tan completa que hasta un inicio de sesión por SSH se queda colgado o no llega a establecerse. Para ese caso necesitas una segunda vía de acceso al servidor.
En los servidores root KVM y en los servidores dedicados de KernelHost esa vía es la consola VNC del área de cliente. Cuelga de la capa de virtualización o de la propia máquina, y sigue estando disponible aunque el servicio SSH ya no responda por falta de memoria. Entra por ahí una vez antes de tocar nada. Una vía de rescate que pruebas por primera vez durante la emergencia no es una vía de rescate.
Haz además una copia del archivo del pool, con marca de tiempo, para que un segundo intento no sobrescriba la primera copia:
mkdir -p /root/backups
cp -a /etc/php/8.2/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)
ls -l /root/backups/
La vuelta atrás son entonces tres líneas:
cp -a /root/backups/www.conf.2026-09-03-1015 /etc/php/8.2/fpm/pool.d/www.conf
php-fpm8.2 -t
systemctl reload php8.2-fpm
Una red de seguridad por si te equivocas en el cálculo
Un límite de memoria en la unidad de systemd hace que un cálculo equivocado se lleve por delante un proceso de trabajo de PHP y no la base de datos: el kernel mata entonces dentro del cgroup de FPM, en lugar de buscar el proceso más grande de todo el sistema.
mkdir -p /etc/systemd/system/php8.2-fpm.service.d
cat > /etc/systemd/system/php8.2-fpm.service.d/memoria.conf <<'EOF'
[Service]
MemoryHigh=1500M
MemoryMax=2G
EOF
systemctl daemon-reload
systemctl restart php8.2-fpm
Comprueba que ha surtido efecto:
systemctl show php8.2-fpm -p MemoryHigh -p MemoryMax
MemoryHigh frena y libera memoria, MemoryMax es el límite duro. Los cuatro sistemas usan la jerarquía unificada de cgroups, así que los valores surten efecto de inmediato. Es una red de seguridad, no un sustituto del cálculo: si se alcanza el límite, el visitante afectado sigue viendo un error, solo que ya no lo ve el servidor entero. Para guardar bien este tipo de archivos complementarios: crear un servicio systemd propio.
Y la tercera regla, que no necesita ningún comando: un cambio cada vez. Si tocas a la vez el modo de funcionamiento, pm.max_children y los valores spare, después no sabrás qué ha hecho efecto.
Inventario: qué pool manda de verdad
Cada pool tiene su propio pm.max_children, y para la memoria lo que cuenta es la suma de todos los pools:
grep -n "^pm" /etc/php/*/fpm/pool.d/*.conf
Recién instalados, los cuatro sistemas traen exactamente lo mismo:
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
No son recomendaciones, sino valores de relleno con los que PHP arranca incluso en una máquina de pruebas muy pequeña. A eso se suma un límite global que abarca todos los pools:
grep -n "^process.max" /etc/php/*/fpm/php-fpm.conf
Si la salida queda vacía, la línea está comentada y rige el valor por defecto 0, es decir, ningún límite global. En servidores con muchos pools es justamente esa línea la que evita que la suma reviente la memoria en cuanto varios pools reciben carga a la vez.
Al final lo que manda no es el archivo, sino lo que FPM hace con él. La opción -tt imprime la configuración completamente resuelta, incluidos todos los valores por defecto que no aparecen en ningún archivo:
php-fpm8.2 -tt 2>&1 | grep -E "^\[|pm |pm\.|listen ="
Esa salida sirve además, más adelante, como prueba de que un cambio ha llegado a aplicarse.
Medir la memoria que consume un proceso de trabajo
Aquí es donde la mayoría de las guías se vuelven imprecisas. Hay tres cifras que se confunden una y otra vez:
memory_limit, 128M de fábrica cuando se ejecuta bajo FPM: es un tope por petición, no un consumo. Un script que necesita 12 MB sigue necesitando 12 MB aunque pongas 512M.- RSS: todo lo que hay en ese momento en la RAM, incluidos el OPcache compartido y las bibliotecas compartidas. Quien suma el RSS de 20 procesos cuenta el mismo OPcache veinte veces.
- PSS: las páginas compartidas se dividen entre el número de procesos que las usan. Es la única de las tres cifras que se puede sumar con sentido.
Para el cálculo necesitas el PSS. El kernel lo entrega ya agregado en /proc/<pid>/smaps_rollup, legible como root:
pgrep -f "php-fpm: pool www" | while read -r p; do
awk '/^Pss:/ {print $2}' "/proc/$p/smaps_rollup"
done | awk '{s+=$1; n++} END {
if (!n) { print "no se han encontrado procesos de trabajo"; exit }
printf "Procesos: %d Suma: %.0f MiB Media: %.1f MiB\n", n, s/1024, s/1024/n
}'
Para comparar, el mismo pool medido con RSS:
ps -eo pid,rss,args --sort=-rss | grep '[p]hp-fpm' | head
La suma de RSS queda bastante más alta según el tamaño del OPcache. Quien calcula con ella acaba con un pm.max_children demasiado pequeño y se compra un tiempo de espera que no haría ninguna falta.
Hay dos condiciones que deciden si la medición vale algo. Mide bajo carga real, no justo después de un reinicio: un proceso recién creado sale barato, porque al principio solo comparte las páginas de memoria del proceso principal, y no se encarece hasta las primeras peticiones. Y no te quedes solo con la media: si la media está en 60 MiB y el proceso más grande en 190 MiB, no calcules con 60.
La segunda fuente: el log de accesos de FPM
Si se lo pides, FPM registra por cada petición el consumo máximo y el tiempo de ejecución. Las dos líneas están comentadas en el archivo del pool:
access.log = /var/log/php8.2-fpm.access.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"
La grafía %{mili}d es correcta tal cual: viene sin cambios del archivo que trae el paquete y devuelve el tiempo de ejecución en milisegundos, mientras que %{kilo}M da el consumo máximo en kilobytes. Después comprueba, recarga y mira si el archivo se crea:
php-fpm8.2 -t
systemctl reload php8.2-fpm
ls -l /var/log/php8.2-fpm.access.log
Deja el log funcionando un día entero para que incluya las horas punta. Después evalúa el consumo máximo por cuantiles:
awk '{print $(NF-1)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
if (!NR) exit
p=int(NR*0.95); if (p<1) p=1
printf "Peticiones: %d Mediana: %d kB p95: %d kB Máximo: %d kB\n", NR, v[int((NR+1)/2)], v[p], v[NR]
}'
La misma evaluación para el tiempo de ejecución, que necesitarás enseguida para el segundo cálculo, toma la columna anterior:
awk '{print $(NF-2)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
if (!NR) exit
p=int(NR*0.95); if (p<1) p=1
printf "Mediana: %.0f ms p95: %.0f ms Máximo: %.0f ms\n", v[int((NR+1)/2)], v[p], v[NR]
}'
Dos limitaciones. Primera: las posiciones de los campos dependen de la línea de formato mostrada arriba, porque esa línea termina con tiempo de ejecución, memoria y porcentaje de CPU. Si cambias access.format, tendrás que ajustarlas. Segunda: %{kilo}M es la contabilidad de memoria del propio PHP y, por tanto, una cota inferior: no incluye el código del programa, las extensiones ni la memoria que la biblioteca de C no devuelve de inmediato tras una petición. Por eso la fórmula usa PSS. El log de accesos sirve para encontrar los scripts atípicos que dejan un proceso grande de forma permanente.
Después vuelve a desactivar el log o configúrale una rotación. La regla que viene incluida solo cubre el log de errores, y un disco lleno genera síntomas que ya no tienen nada que ver con PHP-FPM: liberar espacio cuando el disco está lleno en Linux.
El cálculo
Hay dos cifras: un tope que sale de la RAM y una necesidad real que sale de la carga. El valor correcto es el menor de los dos.
El tope que sale de la RAM
pm.max_children = presupuesto para PHP / PSS por proceso de trabajo
El presupuesto no es toda la RAM:
free -m
La columna available ya tiene en cuenta la caché que se puede liberar, pero no incluye la memoria que tus procesos de trabajo están ocupando ahora mismo. El presupuesto es, por tanto, available más la suma de PSS medida, menos una reserva. Como reserva funciona bien alrededor del 20% de la memoria total, y nunca menos de 512 MB. Esa reserva absorbe todo lo que no aparece en ninguna medición: un buffer pool de la base de datos que crece, un backup, una actualización de paquetes, una importación masiva en el peor momento.
| RAM total | Ocupado de forma permanente | Reserva | Presupuesto para PHP | PSS por proceso | pm.max_children |
| 4 GB | 1,5 GB | 0,8 GB | 1,7 GB | 60 MB | 29 |
| 8 GB | 3,0 GB | 1,6 GB | 3,4 GB | 80 MB | 43 |
| 16 GB | 6,0 GB | 3,2 GB | 6,8 GB | 110 MB | 63 |
Redondea siempre hacia abajo. Un proceso más no aporta nada, y un proceso de más puede costarte todo.
La necesidad que sale de la carga
procesos necesarios = peticiones por segundo × tiempo medio de ejecución en segundos
Las peticiones por segundo salen de la página de estado de FPM: accepted conn dividido entre start since da la media desde el último arranque. El tiempo de ejecución viene de la evaluación de arriba, y en concreto el valor p95, no la mediana, porque si no estarás dimensionando para la tarde tranquila.
Ejemplo: 40 peticiones por segundo en el pico y un tiempo p95 de 300 milisegundos dan 12 peticiones simultáneas, con un margen para picos cortos entre 20 y 25. Si el tope está en 43, escribe el valor de la necesidad real y deja el resto de la memoria donde más rinde, es decir, en la caché de la base de datos.
Si la necesidad queda por encima del tope, aun así no la escribas. Entonces, o la RAM es demasiado pequeña, o los scripts son demasiado lentos, o hay peticiones pasando por PHP que podrían servirse como archivo estático o desde una caché. Las tres causas tienen arreglo; un valor demasiado alto, no: solo retrasa el momento de la caída.
dynamic, ondemand o static
El modo de funcionamiento determina cuándo nacen y desaparecen los procesos. El tope pm.max_children rige en los tres casos.
| Modo | Procesos al arrancar | Comportamiento | Memoria | Encaja con |
| dynamic | pm.start_servers | mantiene entre min_spare y max_spare procesos inactivos listos y crea procesos nuevos hasta max_children cuando hace falta | fluctúa con la carga | el caso normal: uno o pocos pools, carga variable |
| ondemand | ninguno | no arranca un proceso hasta que llega una petición y lo termina de nuevo tras pm.process_idle_timeout | la más baja en reposo | muchos pools en un servidor, sitios con poco tráfico |
| static | pm.max_children | exactamente ese número, de forma permanente, sin crear ni terminar procesos durante el servicio | constante en el máximo | un solo pool en hardware pensado para ello, carga uniforme |
dynamic es la opción por defecto y la correcta para el caso normal. Cuesta algo de CPU al crear procesos y exige que los cuatro valores encajen entre sí.
ondemand ahorra bastante memoria en reposo cuando hay una docena de pools de sitios que se visitan poco en una misma máquina. El precio es la primera petición tras un periodo de calma, que tiene que esperar a que se cree el proceso. pm.process_idle_timeout controla cuánto sobrevive un proceso inactivo (diez segundos de fábrica) y solo actúa en este modo. Ten en cuenta además que aquí el aviso de saturación menciona max_children sin el prefijo pm.. Quien busque el texto habitual no encontrará nada.
static es honesto: lo que escribas se ocupa de inmediato y para siempre, y a cambio ya no hay sorpresas bajo carga. Tiene sentido cuando PHP es el consumidor principal de la máquina. Si PHP comparte la RAM con una base de datos, static le quita a esta la posibilidad de mantener temporalmente más caché.
Independientemente del modo, merece la pena mirar pm.max_requests, 0 de fábrica y por tanto sin límite. Un valor como 500 reemplaza cada proceso de trabajo después de 500 peticiones. Contra un consumo de memoria que crece poco a poco por culpa de una extensión mal hecha es una medida eficaz y barata, porque el OPcache sigue siendo compartido y no se reconstruye. Eso sí, no pongas 20, o FPM se pasará el tiempo creando procesos nuevos.
start_servers y los valores spare
Estos tres valores solo actúan con pm = dynamic y deciden con qué rapidez reacciona FPM ante un pico de carga:
pm.min_spare_servers: como mínimo, estos procesos inactivos mantiene FPM disponibles, el colchón para los picos. Demasiado bajo significa que cada pico tiene que esperar primero a que se creen procesos.pm.max_spare_servers: como máximo, estos procesos inactivos tolera FPM. Sin ese límite, FPM se quedaría con todos los procesos después de un pico y, con ellos, con su memoria.pm.start_servers: estos son los procesos que existen justo después del arranque.
Al arrancar, FPM comprueba cuatro condiciones y se niega a funcionar si alguna no se cumple: los dos valores spare tienen que ser mayores que cero, ninguno de los dos puede superar pm.max_children, max_spare no puede ser menor que min_spare y start_servers tiene que quedar entre ambos. Si pm.start_servers falta por completo, FPM lo calcula por su cuenta y lo registra:
NOTICE: [pool www] pm.start_servers is not set. It's been set to 3.
La fórmula es min_spare + (max_spare - min_spare) / 2, es decir, el punto medio entre los dos valores spare. Como punto de partida, en la práctica funciona esto:
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 6
pm.max_spare_servers = 16
Se pasa por alto con facilidad: los procesos inactivos también ocupan memoria. El presupuesto tiene que soportar pm.max_children, mientras que el consumo del día a día equivale más o menos a max_spare más los procesos activos. Un max_spare alto mantiene la memoria ocupada también a las tres de la madrugada.
La relación con el swap
Es tentador meter el swap en el cálculo. No lo hagas. Un proceso de trabajo cuyos datos están en el disco responde órdenes de magnitud más despacio y, por eso, se queda ocupado más tiempo. El número de peticiones simultáneas sube, FPM crea más procesos y estos desplazan aún más memoria. Esa realimentación es la razón por la que un servidor sobrecargado no va empeorando poco a poco, sino que se cae en cuestión de minutos.
Lo que el swap sí aporta es un colchón para cuando algo falla. Sin él, una sobrecarga termina de golpe con el kernel matando un proceso. Con él tienes primero un servidor lento y, con ello, una ventana de tiempo para intervenir. La regla es: swap sí, pero pm.max_children se calcula exclusivamente contra la RAM real, nunca contra la suma de RAM y swap.
Si ya estás usando el swap se ve en dos sitios. free -m muestra la ocupación, pero no dice nada de la actividad, porque una página que se movió al swap una vez y nunca más hizo falta es inofensiva. Lo que sí es concluyente son las columnas si y so, es decir, las páginas que entran y salen del swap por segundo:
free -m
vmstat 1 5
Valores por encima de cero de forma continuada significan que el servidor está trabajando contra el disco. Si tu kernel trae el indicador de presión de memoria, este es todavía más directo, porque no dice cuánto se ha movido al swap, sino cuánto tiempo han tenido que esperar los procesos por ese motivo:
cat /proc/pressure/memory
Si el archivo no existe, la función está desactivada en el kernel y te quedas con vmstat. Cómo crear swap, dejarlo fijo y ajustar vm.swappiness correctamente está en el artículo configurar swap y evitar caídas por falta de memoria.
Valor demasiado alto: el OOM killer en lugar de una cola
Supongamos que escribes 200 para que el aviso desaparezca de una vez. En reposo no pasa nada, la página carga, todo parece resuelto. En la siguiente avalancha FPM crea de verdad hasta 200 procesos, cada uno crece hasta su tamaño real con las primeras peticiones, la memoria libre baja, el kernel tira primero la caché de archivos (con lo que la base de datos se vuelve más lenta y sus consultas tardan más), después empieza a mover páginas al swap y entonces entra en acción el OOM killer.
Este elige a su víctima según el consumo de memoria, y el proceso individual más grande en un servidor web no es un proceso de trabajo de PHP con 80 MB, sino la base de datos con su buffer pool:
dmesg -T | grep -iE "out of memory|oom-kill"
journalctl -k --since "24 hours ago" | grep -i "out of memory"
Las dos líneas que importan tienen este aspecto:
php-fpm8.2 invoked oom-killer: gfp_mask=0x1100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
Out of memory: Killed process 1234 (mariadbd) total-vm:2891234kB, anon-rss:1783456kB,
file-rss:0kB, shmem-rss:0kB, UID:107 pgtables:4096kB oom_score_adj:0
Aquí el causante y la víctima son dos procesos distintos, y eso es justo lo que vuelve incómoda la búsqueda del fallo: en el buzón hay un aviso sobre una base de datos caída y nadie piensa en la configuración de PHP de anteayer. Según la base de datos, entre paréntesis aparece mariadbd o mysqld.
Si le toca a un proceso de trabajo, FPM lo comunica por su cuenta y, con el límite de memoria puesto, aparece además en el journal:
WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start
php8.2-fpm.service: A process of this unit has been killed by the OOM killer.
La diferencia entre los dos síntomas es el verdadero motivo de este artículo. Un pm.max_children demasiado bajo genera una cola: medible, documentada en el log, atribuible a un valor concreto, y el servidor sigue respondiendo. Uno demasiado alto genera un proceso muerto en un sitio que tú no has elegido, en un servicio que quizá no vuelva por sí solo. El tiempo de espera es un estado de funcionamiento, un evento OOM es un incidente.
Errores frecuentes y soluciones
WARNING: [pool www] server reached pm.max_children setting (5), consider raising it: en algún momento todos los procesos de trabajo estaban ocupados. La línea aparece una vez por fase de saturación, así que una sola línea puede significar una hora entera al máximo. Sube el valor solo después de haber medido el PSS y determinado el presupuesto.
WARNING: [pool www] server reached max_children setting (5), consider raising it: la misma situación con pm = ondemand, redactada sin el prefijo pm..
ALERT: [pool www] pm.min_spare_servers and pm.max_spare_servers cannot be greater than pm.max_children, seguido de ERROR: failed to post process the configuration y ERROR: FPM initialization failed: es el caso más frecuente al bajar pm.max_children. Quien pasa de 50 a 8 y deja pm.max_spare_servers = 20 tal cual se queda después con un servicio que ya no arranca. Los cuatro valores se cambian juntos.
ALERT: [pool www] pm.start_servers must not be less than pm.min_spare_servers and not greater than pm.max_spare_servers: pm.start_servers queda fuera del rango. Corrige el valor o borra la línea, y entonces lo calcula FPM por su cuenta.
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes): esto es memory_limit y no tiene nada que ver con pm.max_children. Un único script ha pedido más de lo que PHP permite por petición. Un pm.max_children más alto no cambia nada, y al revés, un memory_limit más bajo no reduce la memoria que necesita un proceso de trabajo, solo aborta los scripts antes. Para la planificación sigue valiendo esto: memory_limit es el tope de lo que un proceso puede llegar a pedir.
connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream en el log de errores de nginx: todos los procesos ocupados y, además, la cola de aceptación llena. Un listen.backlog más alto solo lo aplaza, la causa está en el número de procesos o en el tiempo de ejecución de los scripts.
WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start: el proceso ha sido terminado a la fuerza desde fuera, casi siempre por el OOM killer. Aquí el valor es demasiado alto, no demasiado bajo. La contraprueba está en el log del kernel.
"He subido el valor y no cambia nada." Casi siempre se ha editado el archivo equivocado: una segunda versión de PHP bajo /etc/php/, un segundo pool o una copia en pool.d/ que ni siquiera se lee. Lo que manda es a qué socket se dirige nginx y qué pool escucha en él:
grep -Rn "fastcgi_pass" /etc/nginx/
grep -n "^listen *=" /etc/php/*/fpm/pool.d/*.conf
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"
"Después del reload la web ha desaparecido." Un reload reinicia FPM con la nueva configuración y, si esta no es válida, el proceso principal termina. Restaura la copia de /root/backups y acostúmbrate a ejecutar php-fpm8.2 -t antes de cada reload.
Cómo saber que el valor es el correcto
"El aviso ha desaparecido" no es ninguna prueba. Con el valor 500 también desaparece, hasta el primer evento OOM. Hay cinco comprobaciones que sí son sólidas.
Primera, la página de estado de FPM. Añade pm.status_path = /status al archivo del pool, comprueba, recarga y consulta el socket directamente, saltándote nginx por completo. Así la página no queda accesible desde internet:
apt-get install -y libfcgi-bin
SCRIPT_NAME=/status SCRIPT_FILENAME=/status REQUEST_METHOD=GET \
cgi-fcgi -bind -connect /run/php/php8.2-fpm.sock
Cuatro líneas de la salida contienen la respuesta:
| Campo | Significado | Valor objetivo |
| max children reached | cuántas veces se ha alcanzado el tope desde el arranque | 0 |
| max listen queue | la cola más larga observada en el socket | 0 |
| max active processes | el número máximo de procesos activos a la vez | bastante por debajo de pm.max_children |
| slow requests | peticiones por encima de request_slowlog_timeout | 0 a ser posible |
Esto no resulta concluyente hasta pasada una semana entera con todas las horas punta. Si después max active processes está en 12 mientras pm.max_children vale 43, tienes margen de sobra y puedes usar esa reserva en otro sitio.
Segunda, la memoria bajo carga, no de madrugada, sino en el pico. available tiene que quedarse claramente por encima de cero y las columnas si y so deben marcar cero:
free -m
vmstat 1 5
Tercera: ningún evento OOM. Esta comprobación es la más importante, porque descarta el peor de los síntomas. Una salida vacía es el resultado deseado:
journalctl -k --since "7 days ago" | grep -i "out of memory"
Cuarta, la suma de todos los pools. En un servidor con varios sitios no cuenta el valor individual, sino la suma, multiplicada por el PSS de cada proceso. Tiene que caber en el presupuesto aunque todos los pools reciban carga a la vez:
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"
Quinta: sobrevive a un reinicio. Es el punto que más se salta la gente. Un valor en un archivo que ni siquiera se lee no se nota hasta el siguiente reinicio, y ese rara vez ocurre justo cuando estás mirando:
systemctl is-enabled php8.2-fpm
systemctl restart php8.2-fpm
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"
Después reinicia el servidor una vez de forma controlada, mientras sigues delante. En los servidores root KVM y en los servidores dedicados de KernelHost lanzas ese reinicio desde el área de cliente y sigues el arranque a través de la consola VNC, aunque el servicio web todavía no responda. Los servidores están en el centro de datos maincubes de Frankfurt am Main.
Lista de comprobación rápida
- Copia del archivo del pool en
/root/backupsy acceso por la consola VNC probado una vez. - Medir el PSS por proceso de trabajo bajo carga real, no después de un reinicio, y no calcular con RSS.
- Determinar el presupuesto:
availablemás la suma de PSS actual, menos una reserva fijada a conciencia. - Calcular el tope, contrastarlo con la necesidad que sale de peticiones por segundo por tiempo p95, quedarse con el valor menor y redondear hacia abajo.
- Elegir el modo de funcionamiento y ajustar los valores spare junto con
pm.max_children. php-fpm8.2 -t, despuéssystemctl reloady despuésphp-fpm8.2 -ttcomo prueba de que el valor está cargado.- Una semana después, revisar
max children reached,max listen queuey el log del kernel.
Preguntas frecuentes
¿Cómo calculo correctamente pm.max_children?
¿Por qué un valor demasiado alto es más peligroso que uno demasiado bajo?
¿Por qué debo calcular con PSS y no con RSS?
¿Cuándo uso dynamic, cuándo ondemand y cuándo static?
¿Puedo incluir el swap en el cálculo?
He subido pm.max_children y no cambia nada. ¿A qué se debe?
Después de bajar pm.max_children, PHP-FPM ya no arranca. ¿Qué ha pasado?
¿Tiene memory_limit algo que ver con pm.max_children?
2026 KernelHost GmbH. Todos los derechos reservados. Esta guía está protegida por derechos de autor. Su publicación en otros sitios web, aunque sea de forma parcial o modificada, no está permitida sin nuestro consentimiento por escrito. Las citas con indicación de la fuente y un enlace son muy bienvenidas.

