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

Aviso de afiliados v3.1
Aviso de afiliados v3.2 (móvil)
Entorno de prueba Familia RHELAlmaLinux 9.8Cuenta rootVerificado el 2026-10-12

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'
58
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-21
09-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:44

Las 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.05
free -m
               total        used        free      shared  buff/cache   available
Mem:           15783       11658         397         340        4383        4124
Swap:           8191        1615        6576
nproc
4

Los 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.x
grep '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 404

A 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.

🧭 Simulador de la ruta de solicitudesPanel de administración wp-adminSimulador explicativo
šŸ—‚ļø Solicitudes que llenaron los workers
Solicitudes wp-admin de bots
Al pedir una dirección de administración sin sesión iniciada, WordPress responde con un 302 a la pÔgina de acceso. Ese 302 es una ejecución de PHP.
EscƔner: archivos tipo .env
Las solicitudes de archivos como .env las corta la regla de bloqueo de nginx con un 444. 0,04 segundos, sin pasar por PHP.
EscƔner: rutas falsas normales
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. 266KB y 1,4–2,9 segundos, el mismo coste que generar una entrada normal.
Precarga de cachƩ
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. Hasta 60 por minuto, y cada solicitud usa un worker.
Visita a una entrada en cachƩ
Si existe el archivo de cachƩ, nginx responde directamente sin pasar por PHP.
Visita a una entrada sin cachƩ
Si no hay archivo de cachƩ, PHP genera la pƔgina y guarda ese HTML como archivo de cachƩ.
Flujo completo
La solicitud baja de arriba abajo y se detiene en la capa donde termina la respuesta. Solo las solicitudes que llegan hasta WordPress usan un worker de PHP.
PeticiónSe detiene enRespuestaWorkers
wp-admin de botsWordPress302 (a la pƔgina de acceso)Usa 1
EscƔner tipo .envRegla de bloqueo de nginx444 (0,04 s)No usa
EscĆ”ner ruta falsa normalWordPressPĆ”gina 404 (266KB Ā· 1,4–2,9 s)Usa 1
Precarga de cachéWordPress200 · 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éWordPress200 · guardado como archivo de cachéUsa 1
Simulador explicativo que funciona solo en tu navegador. Las respuestas y los tiempos proceden de los logs y mediciones de un servidor con Nginx + Apache en octubre de 2026.

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 -c

Si 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.

šŸ“ŠAntes y despuĆ©s del lĆ­mite de velocidadPor dĆ­a Ā· recuento de logs
AntesDespuƩs
Respuestas 302 de administración
302 que WordPress envió a solicitudes wp-admin sin sesión. El límite para direcciones de administración sin sesión se aplicó la noche del 9 de octubre.
02,4994,9987,4979,996
10/99,996Antes
10/1011DespuƩs
10/111DespuƩs
Solicitudes de la IP del bot
Solicitudes diarias de la IP que envió 22.391 en tres horas.
05,59811,19616,79322,391
10/922,391Antes
10/100Sin solicitudes
10/110Sin solicitudes
PƔginas 404 generadas por WordPress
Solicitudes del escÔner a rutas falsas normales. El límite de solicitudes que llegan a PHP se aplicó la noche del 11 de octubre, así que se valorarÔ con la revisión de la próxima semana.
06531,3061,9592,612
10/92,563Antes
10/102,612Antes
10/111,630Antes Ā· aplicado la noche del 11
Las cifras son recuentos diarios del access.log de nginx del 9 al 11 de octubre de 2026.

šŸ”¢ FAQ & contenido recomendado

dynamic crea y detiene workers según el volumen de solicitudes y registra el aviso seems busy cuando los workers en espera llegan a 0. static mantiene siempre max_children workers, así que no hay retraso de creación ni avisos, pero ocupa memoria de forma permanente. El servidor de este artículo se quedó en dynamic con 26; si tienes memoria de sobra y un volumen de solicitudes constante todo el día, static es lo adecuado.

Actívalo cuando en las horas del aviso parezca que ciertas solicitudes tardan mucho. Si en la configuración del pool indicas la ruta [slowlog] y [request_slowlog_timeout], se registra en qué punto se ejecutaba cada solicitud que supere ese tiempo. En este caso la causa era el número de solicitudes, así que bastó con contar access.log y no se usó slowlog.

El límite solo se aplica a solicitudes sin sesión iniciada que no pasan por la caché y llegan a PHP, y solo el exceso recibe un 429 cuando una IP supera 30 por minuto o 60 de golpe. En el recuento del 11 de octubre, la única IP que superó ese umbral fue un escÔner, y el exceso se levanta en menos de un minuto, así que la siguiente solicitud recibe una respuesta normal.

No la reduje. La precarga se concentró en las horas en que se guardaban entradas y traducciones y no provocó errores a los visitantes. El criterio no es el número de avisos, sino si los visitantes recibieron errores 502 o 504; si esa cifra es 0, la precarga se deja como estÔ.

ā„¹ļø 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.

Deja una respuesta

Tu dirección de correo electrónico no serÔ publicada. Los campos obligatorios estÔn marcados con *

Demuestra que eres humano: 10   +   7   =