Aviso de php-fpm max_children: encuentra la causa en los logs antes de aƱadir workers

El aviso php-fpm max_children indica que todos los procesos worker de PHP estĆ”n ocupados atendiendo solicitudes, de modo que las nuevas entran en cola. En un servidor WordPress con Nginx, Apache y php-fpm donde este aviso llevaba semanas acumulĆ”ndose, identifiquĆ© en los logs las solicitudes que llenaban los workers y lo resolvĆ con un lĆmite de velocidad en nginx en lugar de subir el valor.
En la revisión semanal el aviso se registró entre 27 y 187 veces al dĆa, pero los visitantes recibieron 0 errores 502 o 504 en esos cuatro dĆas. Lo que llenaba los workers no eran visitantes, sino un bot que pedĆa una y otra vez direcciones de administración y un escĆ”ner que solicitaba rutas inexistentes.
1ļøā£ php-fpm max_children & seems busy
Cuando el aviso se acumula hasta 66 al dĆa, es fĆ”cil pensar que faltaron workers 66 veces. El log de php-fpm mezcla dos avisos de distinta naturaleza, y la mayorĆa son avisos previos que se registran antes de llegar al lĆmite.
[09-Oct-2026 00:16:18] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 15 total children
[09-Oct-2026 13:02:05] WARNING: [pool www] server reached pm.max_children setting (26), consider raising it[seems busy] avisa de que hay 0 workers en espera y se estĆ” creando un proceso nuevo; con la gestión de procesos en dynamic, basta un breve aumento de solicitudes para que se registre. [server reached pm.max_children] es el momento en que se alcanzó el lĆmite de 26 y no se pudieron crear mĆ”s, y a partir de ahĆ las nuevas solicitudes esperan.
Es la diferencia entre llamar a un camarero mƔs y dejar a los clientes esperando fuera porque todas las mesas estƔn ocupadas.
Al contar un dĆa por tipo salieron 58 seems busy y 6 max_children. La rutina de revisión sumó ambos avisos y reportó 66, pero el lĆmite se alcanzó realmente seis veces.
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'pool www' | grep -c 'seems busy'58grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-2109-Oct-2026 13:02:05
09-Oct-2026 13:52:10
09-Oct-2026 18:09:20
09-Oct-2026 18:42:17
09-Oct-2026 18:46:07
09-Oct-2026 19:12:44Las seis horas que muestra el segundo comando son la referencia para rastrear la causa; el siguiente paso usa solo esas horas.
2ļøā£ Por quĆ© subir max_children no es la solución
Subir el lĆmite de 26 puede reducir el nĆŗmero de avisos, pero no cambia quĆ© solicitudes llenaron los workers.
El cĆ”lculo habitual divide la memoria disponible entre la memoria media de un worker. En el servidor medido, 26 workers usaban 131MB de media y 214MB como mĆ”ximo, 3,4GB en total, y de los 15,7GB de memoria habĆa 4,1GB disponibles, asĆ que sobre el papel se podrĆan crear 30 o mĆ”s.
ps -o rss= -u www -C php-fpm | awk '{s+=$1; n++; if($1>m) m=$1} END{print n, s/n/1024, m/1024, s/1024}'26 130.579 214.27 3395.05free -m total used free shared buff/cache available
Mem: 15783 11658 397 340 4383 4124
Swap: 8191 1615 6576nproc4Los cuatro valores de la primera salida son el número de workers, la memoria media (MB), la mÔxima (MB) y el total (MB); la columna available de la salida de free es la memoria disponible.
Con 4 nĆŗcleos de CPU, mĆ”s workers no aumentan las solicitudes atendidas a la vez; solo alargan lo que tarda cada una. Mientras los visitantes reciben 0 errores 502 o 504, subir el lĆmite no aporta nada y solo da mĆ”s procesos a los bots y escĆ”neres que ocupaban los workers.
Antes de subir el lĆmite, comprueba si las solicitudes que ocupaban los workers en las horas del aviso eran de visitantes o no; el siguiente apartado explica cómo verlo en access.log.
3ļøā£ Rastrear la causa en access.log: hora, IP, ruta y código de respuesta
Elige una hora de aviso y la respuesta estĆ” en el access.log de esos 1ā2 minutos. El orden es: extraer las lĆneas por hora, contar por IP y revisar las rutas y los códigos de respuesta de las IP principales.
grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head 115 140.245.x.x
61 187.15.x.x
17 3.36.x.x
8 91.239.x.x
4 79.127.x.x
3 109.248.x.x
3 109.248.x.x
3 109.248.x.x
2 79.127.x.x
2 40.77.x.xgrep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head 55 / 200
13 /wp-login.php 404
13 /wp-admin/plugins.php 302
13 /wp-admin/index.php 302
13 /wp-admin/edit.php 302
12 /wp-admin/profile.php 302
4 / 499
3 /xmlrpc.php 302
2 /xmlrpc.php 499
2 /wp-content/themes/kinsta/images/duos/small/wheelchair.svg 404A las 18:09 del 9 de octubre se registraron 286 solicitudes en un minuto; una sola IP hizo 115, las rutas eran direcciones de administración como /wp-admin/profile.php y todos los códigos de respuesta eran 302.
El código de respuesta indica si se llegó a PHP. Un 200 de HTML en caché servido directamente por nginx y los 444 o 400 no pasan por PHP, mientras que 302, 404 y un 200 sin caché usan un worker cada uno. Al pedir una dirección de administración sin sesión iniciada, WordPress responde con un 302 a la pÔgina de acceso, y ese 302 es una ejecución de PHP.
Si un servidor aloja dos o mĆ”s sitios, el access.log global registra las lĆneas de ambos. Al principio pensĆ© que este bot atacaba el primer sitio, pero al cotejarlo con el log de cada sitio resultó que 22.390 de las 22.391 solicitudes eran del segundo sitio.
4ļøā£ Tres tipos de solicitudes que llenaron los workers: wp-admin de bots, 404 de escĆ”neres y precarga de cachĆ©
Al aplicar el mismo orden a todas las horas con aviso, las solicitudes que llenaron los workers fueron de tres tipos.
La primera era un bot que pedĆa una y otra vez direcciones de administración. Una IP envió 22.391 solicitudes en tres horas; 9.944 fueron a cuatro rutas, entre ellas /wp-admin/profile.php, y como cada 302 sin sesión es una ejecución de PHP, usó un worker 9.944 veces.
La segunda era un escĆ”ner de vulnerabilidades que pedĆa rutas inexistentes. Aunque el User-Agent diga Googlebot, no lo es: la IP que envió 14.672 solicitudes al dĆa era un servidor virtual de Google Cloud cuyo DNS inverso termina en googleusercontent.com, mientras que Googlebot termina en googlebot.com. La misma IP alternaba entre bingbot, Applebot, GPTBot y ClaudeBot.
Las solicitudes de archivos como .env las corta nginx con un 444 en 0,04 segundos, pero las rutas normales que no estĆ”n en las reglas de bloqueo, como /userfiles o /secrets.yml, hacen que WordPress genere una pĆ”gina 404. Esa pĆ”gina 404 cuesta 266KB y 1,4ā2,9 segundos, lo mismo que generar una entrada normal, y una IP la pidió 4.069 veces al dĆa.
La tercera era el propio servidor. Al guardar una entrada o una traducción se vacĆa la cachĆ© y la precarga de WP Rocket vuelve a pedir todas las URL: 8.489 solicitudes al dĆa y hasta 60 por minuto. Las comprobaciones de estado de indexación de Rank Math tambiĆ©n usaron un worker 754 veces al dĆa a travĆ©s de Action Scheduler. Ambas se concentraron en las horas de trabajo y no causaron errores a los visitantes, asĆ que las dejĆ© fuera de la solución.
En el simulador de abajo puedes ver en quƩ capa termina cada solicitud y si usa un worker de PHP.
| Petición | Se detiene en | Respuesta | Workers |
|---|---|---|---|
| wp-admin de bots | WordPress | 302 (a la pƔgina de acceso) | Usa 1 |
| EscƔner tipo .env | Regla de bloqueo de nginx | 444 (0,04 s) | No usa |
| EscĆ”ner ruta falsa normal | WordPress | PĆ”gina 404 (266KB Ā· 1,4ā2,9 s) | Usa 1 |
| Precarga de caché | WordPress | 200 · guardado como archivo de caché | Usa 1 |
| Visita a una entrada en cachƩ | Archivo de cachƩ | 200 (nginx responde directamente) | No usa |
| Visita a una entrada sin caché | WordPress | 200 · guardado como archivo de caché | Usa 1 |
5ļøā£ LĆmite de velocidad en nginx: limitar solicitudes sin sesión y recuperar la IP real
ElegĆ limitar la velocidad en lugar de bloquear. Si bloqueas una ruta, el escĆ”ner cambia de ruta; si bloqueas una IP, al dĆa siguiente llega otra. Solo se limitan las solicitudes sin sesión que llegan a PHP, a 30 por minuto y 60 de golpe por IP, y el exceso recibe un 429.
En un servidor detrÔs de un CDN, esta configuración sola no basta. La IP que ve nginx es la del edge del CDN, asà que todos los visitantes que pasan por ese edge se agrupan en una sola IP; primero hay que registrar la lista de IP de los edges con set_real_ip_from para recuperar la IP real de X-Forwarded-For. Las solicitudes con cookie de sesión quedan exentas, pero no se exime nada por User-Agent.
# contexto http
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24; # lista de edges del CDN
map $http_cookie $php_anon_key {
default $binary_remote_addr;
~*wordpress_logged_in '';
}
limit_req_zone $php_anon_key zone=php_anon:10m rate=30r/m;
# location que envĆa a PHP
location @apache {
limit_req zone=php_anon burst=60 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}Las pĆ”ginas en cachĆ© las responde try_files dentro de nginx y no llegan a esta location, y admin-ajax.php es otra location, asĆ que no entra en el lĆmite.
La prueba se hace con solicitudes simultĆ”neas. En serie, cada 404 tarda 1,5 segundos y nunca se superan 30 por minuto, asĆ que no salta el lĆmite; enviando 100 solicitudes de 20 en 20 salen 67 respuestas 404 y 33 respuestas 429, lo que confirma que el lĆmite funciona.
seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -cSi la salida cuenta respuestas 429, el lĆmite funciona. Si eximiste la IP del propio servidor, quita la exención un momento al probar o ejecĆŗtalo desde otra red.
6ļøā£ Antes y despuĆ©s
El lĆmite para direcciones de administración sin sesión se aplicó la noche del 9 de octubre, y el de solicitudes que llegan a PHP, la noche del 11. Los 302 de administración bajaron de 9.996 el dĆa 9 a 11 el dĆa 10 y 1 el dĆa 11, y la IP que envió 22.391 solicitudes en tres horas no ha enviado ninguna desde el dĆa siguiente.
En el caso del escĆ”ner el lĆmite se acaba de aplicar, asĆ que aĆŗn no hay cifras de un dĆa completo; se valorarĆ” por cuĆ”nto bajen en la revisión de la próxima semana las pĆ”ginas 404 generadas por WordPress (2.563 el dĆa 9, 2.612 el 10 y 1.630 el 11). Los errores 502 y 504 de los visitantes fueron 0 antes y despuĆ©s.
Estas cifras se contaron directamente en los logs de un servidor WordPress con Nginx, Apache y php-fpm, y el criterio no fue el nĆŗmero de avisos sino los errores de los visitantes y las solicitudes que llegan a PHP.
š¢ FAQ & contenido recomendado
CachƩ
CachƩ del sitio web y del servidor
Explica capa por capa cómo las solicitudes en caché evitan PHP.
Seguridad
Vulnerabilidades de seguridad de wp-config.php
Cómo proteger con permisos y reglas del servidor el archivo de configuración al que apuntan los escÔneres.
Linux
15 categorĆas de comandos de Linux
Una tabla que clasifica los comandos en 15 categorĆas, incluidos grep y awk usados para contar los logs.
ā¹ļø Aviso de afiliados
El contenido de este sitio incluye enlaces de afiliado. Cuando un visitante compra un producto o servicio a travĆ©s de uno de ellos, el sitio recibe una comisión del vendedor. El importe que paga el comprador(baja durante las promociones ā)no aumenta. Los precios, descuentos y existencias publicados corresponden al momento de redacción y pueden variar, asĆ que confĆrmalos con el vendedor antes de comprar. Los productos se eligen y evalĆŗan con criterios propios, y las comisiones no influyen en el orden ni en el contenido de las reseƱas.