Optimizar Pool PHP-FPM en VPS Linux: pm.max_children y el Fin del 502

Resuelve el 502 Bad Gateway calculando pm.max_children, pm.start_servers y pm.max_requests en el pool PHP-FPM de tu VPS Linux con Nginx.

Un pool PHP-FPM mal dimensionado es la causa número uno de 502 Bad Gateway en VPS con Nginx + PHP. El síntoma aparece bajo carga moderada — 50, 100 requests simultáneos — y desaparece cuando el tráfico baja, llevando el diagnóstico en direcciones equivocadas (Nginx, base de datos, red). En realidad, el pool está agotado: todos los workers PHP-FPM ocupados, Nginx intenta enviar el request vía FastCGI, recibe rechazo o timeout, y devuelve 502 al cliente.

Este tutorial es para ti, developer o sysadmin con VPS Linux corriendo Nginx + PHP-FPM en cualquier aplicación PHP (Laravel, WordPress, Symfony, Magento) que está viendo 502 esporádicos en producción o simplemente quiere dimensionar el pool a partir de mediciones reales, no de valores copiados de un blog. Vamos a cubrir cómo medir la memoria real de los workers, calcular pm.max_children con la fórmula correcta, ajustar los parámetros auxiliares (pm.start_servers, pm.min_spare_servers, pm.max_spare_servers, pm.max_requests) y validar la configuración con Nginx sirviendo carga real.

Tiempo estimado de ejecución: 25 minutos, incluyendo una prueba de carga controlada para validar antes/después.

Requisitos previos

Necesitas una VPS Linux con privilegios root o sudo, PHP-FPM instalado y funcional, y Nginx ya enrutando requests al socket Unix de PHP-FPM. Los ejemplos usan Ubuntu 24.04 LTS con PHP 8.3, pero los principios se aplican a cualquier versión de PHP-FPM (7.4 en adelante) y a Debian, AlmaLinux o Rocky Linux con ajustes mínimos en las rutas de archivo.

Requisitos previos

Acceso SSH activo, sudo y la aplicación PHP corriendo en producción o staging con tráfico representativo. Vas a necesitar recolectar métricas con el pool bajo carga real — medir en idle da números equivocados.

Sistema Ubuntu 24.04 LTS
PHP 8.3 (php-fpm)
Config del pool /etc/php/8.3/fpm/pool.d/www.conf
Socket Unix /run/php/php8.3-fpm.sock

Medir la memoria real de los workers

Antes de tocar cualquier parámetro, necesitas saber cuánto consume cada worker PHP-FPM de memoria residente (RSS) con tu aplicación específica. Ese número varía drásticamente entre stacks — un worker Laravel con Eloquent pesado puede consumir 80–120 MB, un WordPress sencillo se queda en 40–60 MB, y una API Symfony optimizada puede correr con 30 MB. Adivinar 60 MB porque “es el valor por defecto” es la forma más común de infradimensionar el pool.

01

Genera carga representativa en la aplicación. En staging, usa una herramienta como ab (ApacheBench) o wrk contra los endpoints más pesados:

ab -n 1000 -c 20 https://tu-dominio.com/endpoint-pesado

En producción, puede ser el tráfico natural durante hora pico. El objetivo es forzar a los workers a procesar requests reales y quedar en estado post-ejecución con la aplicación cargada en memoria.

02

Con el pool bajo carga, lista los workers ordenados por RSS:

ps -ylC php-fpm8.3 --sort:rss

La salida tiene una columna RSS en KB. Ignora el proceso master (normalmente el primero, el más pequeño) y enfócate en los workers (estado S tras haber procesado requests). Esos son los que importan para el cálculo.

03

Calcula la media de RSS de los workers activos:

ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1; count++} END {print "Media:", sum/count/1024, "MB"}'

Anota ese valor. En una aplicación Laravel típica, espera algo entre 70 y 110 MB. Ese es el número que va a la fórmula de pm.max_children.

Calcular pm.max_children

La fórmula es simple y existe para ser respetada: pm.max_children = (RAM disponible para PHP) ÷ (RSS medio por worker). El punto no obvio es el “disponible para PHP” — no es la RAM total de la VPS. Tienes que restar lo que MySQL, Redis, Nginx, kernel y buffers consumen en pico.

04

Mira la RAM total y el uso actual:

free -m

En una VPS de 4 GB ejecutando MySQL (~800 MB), Redis (~150 MB), Nginx (~50 MB) y el sistema base (~400 MB), quedan aproximadamente 2,5 GB para PHP. En una VPS de 2 GB con la misma stack, quedan unos 600 MB — mucho menos de lo que los blogs suelen asumir.

05

Aplica la fórmula. Con 2,5 GB para PHP y RSS medio de 80 MB por worker:

2560 MB ÷ 80 MB = 32 workers

Reserva un margen de seguridad del 15–20%. Usa 26 o 27 como pm.max_children. Ese buffer evita que picos de RSS (workers procesando endpoints atípicos) empujen al sistema al swap, que destruye el rendimiento mucho más rápido que un 502.

El swap es peor que el 502

Si agotas la RAM y el sistema cae en swap, la latencia sube a segundos y la VPS entera queda no responsiva. Un 502 ocasional es recuperable; el thrashing de swap lleva a un outage completo. Siempre infradimensiona antes que sobredimensionar.

Ajustar el pool en www.conf

Con pm.max_children calculado, abre /etc/php/8.3/fpm/pool.d/www.conf y ajusta los parámetros relacionados. No son independientes — valores mal alineados causan tanto churn que la ganancia del max_children correcto desaparece en overhead de spawn.

06

Edita el pool:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Localiza y ajusta estas líneas. Los valores de abajo asumen el ejemplo de 26 workers calculado antes — adáptalos a tu cálculo:

pm = dynamic
pm.max_children = 26
pm.start_servers = 7
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500

La regla práctica: start_servers ≈ 25% de max_children, min_spare_servers ≈ 15%, max_spare_servers ≈ 40%. Eso mantiene suficientes workers en idle para absorber picos sin desperdiciar RAM cuando no hay tráfico.

07

Habilita la status page del pool — la vas a necesitar para monitorizar. Descomenta o añade:

pm.status_path = /fpm-status

Guarda y cierra el archivo.

08

Valida la sintaxis antes de aplicar:

sudo php-fpm8.3 -t

La salida debe ser configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful. Si da error, el pool no va a arrancar y el sitio se cae — no sigas con un error pendiente.

09

Recarga PHP-FPM sin matar requests en curso:

sudo systemctl reload php8.3-fpm

Reload es graceful: los workers actuales terminan lo que están procesando, el master spawnea nuevos con la config nueva, y la transición es transparente para el usuario. Restart solo si reload falla.

Exponer el status en Nginx para monitorizar

pm.status_path solo funciona si Nginx enruta el request hacia PHP-FPM. Sin eso dimensionas a ciegas y descubres el problema solo cuando el 502 vuelve a aparecer.

10

Edita el vhost de Nginx y añade un location protegido:

location ~ ^/(fpm-status|fpm-ping)$ {
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
}

La restricción por IP es esencial — un status público expone carga y número de workers a cualquiera en internet, lo que es información útil para un atacante planeando un DoS.

11

Prueba la config y recarga:

sudo nginx -t && sudo systemctl reload nginx

Accede a http://127.0.0.1/fpm-status vía SSH (curl localhost) y confirma que la página devuelve métricas: accepted conn, listen queue, active processes, max active processes.

Verificación

Genera carga de nuevo y observa dos métricas críticas en el status. La primera es listen queue: si es mayor que cero de forma consistente, el pool se está saturando y los requests se están encolando — necesitas aumentar max_children o investigar lentitud en la aplicación. La segunda es max children reached: si ese contador sube, has tocado techo al menos una vez desde el último reload.

curl -s http://127.0.0.1/fpm-status

Salida saludable bajo carga:

pool:                 www
process manager:      dynamic
accepted conn:        4823
listen queue:         0
max listen queue:     2
idle processes:       6
active processes:     11
total processes:      17
max active processes: 19
max children reached: 0

Si max children reached se quedó en 0 durante la prueba de carga, el dimensionamiento es correcto. Si subió, ejecuta ps de nuevo bajo carga, recalcula la RSS media (los workers pueden estar consumiendo más de lo que mediste inicialmente) y ajusta.

Resolución de problemas

El 502 sigue apareciendo tras el ajuste

Revisa /var/log/php8.3-fpm.log buscando la cadena server reached max_children setting. Si aparece, tu cálculo subestimó la memoria o la carga real es mayor que la prueba. Aumenta max_children en un 20% y repite el ciclo de medición. Si el mensaje no aparece pero el 502 persiste, el problema no es el pool — investiga fastcgi_read_timeout en Nginx (el valor por defecto de 60s puede ser bajo para endpoints pesados) y queries lentas en la base de datos.

Workers consumiendo mucha más RAM de la esperada

Los memory leaks en aplicaciones PHP se acumulan a lo largo de cientos de requests. Reduce pm.max_requests de 500 a 200 — eso recicla workers más agresivamente y mata el leak antes de que crezca. Si el leak es muy rápido (el worker duplica su tamaño en 50 requests), el problema es el código — profilea con xdebug.profiler_enable en staging para encontrar la causa.

No uses pm = static sin capacity planning

pm = static asigna todos los workers en el boot de PHP-FPM. Si configuras pm.max_children = 100 en una VPS de 2 GB y usas static, el boot va a consumir toda la RAM inmediatamente y el sistema entra en thrashing antes de que llegue el primer request.

Próximos pasos

Con el pool dimensionado, algunos próximos pasos lógicos para fortalecer la stack: configurar OPcache con un opcache.memory_consumption adecuado para evitar recompilación en cada hit, ajustar fastcgi_read_timeout en Nginx para endpoints conocidos como lentos, exportar métricas del pool a un sistema de observabilidad (Grafana, Datadog), e implementar health checks en Nginx vía fastcgi_next_upstream para failover elegante.

Si estás llevando esto a producción y quieres empezar con una base estable, una VPS Hostini ya viene con PHP-FPM 8.3 preinstalado, Nginx tuneado y SSD NVMe — lo que reduce drásticamente el tiempo de I/O por request e impacta directamente el pm.max_children que tu aplicación necesita.

Preguntas frecuentes

¿Cuál es el valor ideal de pm.max_children?

No existe un valor ideal universal — depende de la RSS media de cada worker y de la RAM disponible. La fórmula es (RAM libre para PHP) ÷ (RSS medio por proceso). En una VPS de 4 GB con 60 MB por worker y 2 GB reservados para PHP, son 33 workers. Mide antes de adivinar.

¿Por qué recibo 502 Bad Gateway solo bajo carga?

Casi siempre es el pool agotado: pm.max_children demasiado bajo o workers bloqueados en I/O. Nginx envía el request, PHP-FPM lo rechaza o tarda más que fastcgi_read_timeout, y Nginx devuelve 502. Revisa php-fpm.log buscando 'server reached max_children setting'.

¿Debo usar pm = dynamic, static u ondemand?

dynamic es el valor por defecto y funciona en el 95% de los casos. static solo tiene sentido en VPS dedicado a una única aplicación con tráfico predecible — elimina el churn de spawn. ondemand ahorra RAM en inactividad pero añade latencia en el primer request — bueno para sitios de bajo tráfico, malo para APIs.

¿pm.max_requests afecta al rendimiento?

Sí. Define cuántos requests procesa cada worker antes de ser reciclado. Un valor bajo (100) mata los leaks de memoria rápido pero aumenta el consumo de CPU en spawn. Un valor alto (1000) es eficiente pero deja crecer los leaks. 500 es un punto medio seguro para la mayoría de stacks (Laravel, WordPress, Symfony).

¿Cómo saber la RSS real de mis workers PHP-FPM?

Ejecuta `ps -ylC php-fpm8.3 --sort:rss` con el pool bajo carga real (no en idle). La columna RSS muestra la memoria residente en KB. Calcula la media de los workers en estado S (sleeping tras request) — ese es el consumo estable que importa para el cálculo de pm.max_children.

¿Necesito reiniciar PHP-FPM o basta con reload?

`systemctl reload php8.3-fpm` es suficiente para cambios en pm.* — relectura graceful sin matar requests en curso. Usa restart solo si cambias opciones globales (error_log, daemonize) o si el pool está bloqueado. Reload es seguro en producción.

Temas:
Próximos pasos Cloud Ryzen con NVMe y protección DDoS siempre activa.Pon en producción en un VPS Hostini →
¿Te resultó útil este tutorial?
Hablar por WhatsApp