Actualizaste a Ubuntu 24.04, todo parecía bien y luego tu sitio empezó a caer en 502 Bad Gateway como si practicara un simulacro de recuperación de desastres. Reinicias php-fpm, funciona unos minutos y luego vuelve a colapsar. Te quedas mirando los registros de Nginx como si te debieran dinero.
La realidad es esta: los fallos de PHP-FPM rara vez son “misteriosos”. Normalmente están a una línea decisiva de registro de ser aburridos. Encuentra esa línea y dejarás de adivinar y empezarás a arreglar.
La línea de registro que debes encontrar (y por qué importa)
Si PHP-FPM se está “bloqueando”, quieres la línea que diga quién lo mató o qué señal lo terminó. No el 502 de Nginx. No la advertencia de WordPress. No la traza de pila de tu app (todavía). La línea que debes encontrar es una de estas:
- Línea del OOM killer del kernel:
Out of memory: Killed process ... (php-fpm...)o entradasoom-kill:. - Línea de resultado de systemd:
Main process exited, code=killed, status=9/KILLostatus=11/SEGV. - Caída de child de PHP-FPM:
child ... exited on signal 11 (SIGSEGV)ochild ... exited with code 255. - Fallos de socket/listen:
unable to bind listening socket,Address already in use,Permission denied.
Esas líneas deciden tu rama de realidad:
- ¿OOM killer? Deja de tunear PHP y empieza a dimensionar memoria,
pm.max_childreny comportamiento de memoria por petición. - ¿SIGSEGV? Trátalo como un fallo nativo: bug de una extensión, caso límite de Opcache JIT, memoria compartida corrupta o un módulo compilado mal.
- ¿Status 9/KILL sin OOM? Probablemente watchdog/timeouts de systemd, scripts administrativos, límites de cgroup o límites de memoria en contenedores.
- ¿Problemas de bind/permiso de socket? Es un error de despliegue/configuración, no un problema de rendimiento.
Una cita que vale la pena poner en una nota adhesiva, porque obliga a disciplina:
Idea parafraseada — Werner Vogels: “Todo falla; construye sistemas que esperen fallos y se recuperen rápido.”
Vamos a hacer la parte de “esperar”: primero consigue la línea correcta, luego abre la caja de herramientas correcta. Tu presupuesto de disponibilidad merece algo mejor que “reiniciar y esperar”.
Guía rápida de diagnóstico (primero/segundo/tercero)
Este es el camino rápido cuando producción está sangrando y no tienes tiempo para danzas interpretativas con los registros.
Primero: confirma qué significa “caerse” según systemd
- Revisa el estado del servicio y el último estado de salida.
- Decide: OOM vs señal vs fallo de configuración vs problema de dependencias.
Segundo: busca al asesino (OOM del kernel, kill de systemd o segfault)
- Busca en
journalctlporoom,killed process,SEGV,SIG. - Si ves OOM: para y arregla memoria y concurrencia. No “optimices PHP” todavía.
Tercero: correlaciona con los logs de Nginx y de pools para conocer el alcance
- ¿Los errores coinciden con picos de tráfico, cron jobs, despliegues o backups?
- ¿Es un pool o todos los pools?
- ¿Las peticiones son lentas/colgadas antes de la muerte? Si sí, habilita slowlog y timeouts.
Eso es todo. Si no encuentras la línea asesina en 10 minutos, probablemente buscas en el lugar equivocado o tus logs están mal enrutados.
Hechos interesantes y contexto (lo que explica lo extraño de hoy)
- PHP-FPM no siempre fue “el PHP por defecto”. FastCGI Process Manager comenzó como un conjunto de parches de terceros antes de integrarse en el core de PHP hace años, lo que explica algunas perillas “legacy” que aún existen.
- systemd cambió el flujo de trabajo de troubleshooting. El journal a menudo tiene la verdad incluso cuando los logs de la app no, porque systemd registra códigos de salida, señales y bucles de reinicio.
- Código de salida 139 suele ser un segfault. En la convención de Linux, 128 + número de señal; señal 11 (SIGSEGV) se convierte en 139. No es magia, es aritmética con consecuencias.
- El OOM killer es una decisión de política, no un bug. El kernel mata algo para mantener el sistema vivo. Si eligió PHP-FPM, te está diciendo qué proceso era “el más matable” bajo presión.
- Opcache es una función de rendimiento que vive en memoria compartida. Cuando se comporta mal, puede tumbar múltiples workers de formas que parecen aleatorias, porque el estado compartido es el denominador común.
- Los sockets Unix son más rápidos, pero más estrictos. Las escuchas TCP son indulgentes con permisos; los sockets Unix no. Un modo/propietario equivocado y Nginx gritará mientras PHP-FPM insiste en que “está en ejecución”.
pm.max_childrenno es “cuántos núcleos CPU tienes”. Es cuántos procesos PHP concurrentes permites, y la memoria suele ser el limitador real mucho antes que la CPU.- Las actualizaciones de Ubuntu pueden cambiar silenciosamente valores por defecto. Nuevas compilaciones de PHP, diferente hardening en unit de systemd y bibliotecas OpenSSL/ICU actualizadas pueden alterar el comportamiento y la estabilidad de extensiones.
Broma #1: PHP-FPM es como una cafetería—si abres las puertas a clientes ilimitados con un solo barista, tu “latencia” se volverá un estilo de vida.
Tareas prácticas: comandos, salidas y decisiones
Querías tareas reales, no sensaciones. Cada tarea abajo incluye: el comando, una salida realista, qué significa y qué decisión tomar.
Tarea 1: Revisar estado del servicio y estado de salida
cr0x@server:~$ systemctl status php8.3-fpm --no-pager
● php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager
Loaded: loaded (/usr/lib/systemd/system/php8.3-fpm.service; enabled; preset: enabled)
Active: failed (Result: signal) since Mon 2025-12-29 09:14:11 UTC; 2min 3s ago
Process: 18244 ExecStart=/usr/sbin/php-fpm8.3 --nodaemonize --fpm-config /etc/php/8.3/fpm/php-fpm.conf (code=killed, signal=SEGV)
Main PID: 18244 (code=killed, signal=SEGV)
CPU: 2.114s
Significado: systemd vio que PHP-FPM murió por SIGSEGV. Esto no es “se volvió lento”. Se bloqueó.
Decisión: Investigar extensiones/Opcache/JIT/core dump en lugar de ajustar timeouts primero.
Tarea 2: Extraer las líneas decisivas del journal (último arranque)
cr0x@server:~$ journalctl -u php8.3-fpm -b -n 200 --no-pager
Dec 29 09:14:11 server systemd[1]: php8.3-fpm.service: Main process exited, code=killed, status=11/SEGV
Dec 29 09:14:11 server systemd[1]: php8.3-fpm.service: Failed with result 'signal'.
Dec 29 09:14:11 server systemd[1]: php8.3-fpm.service: Scheduled restart job, restart counter is at 5.
Dec 29 09:14:11 server systemd[1]: Stopped php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager.
Significado: Segfault repetido; systemd está en bucle de reinicio.
Decisión: Estabilizar deshabilitando funciones sospechosas (JIT), considerar coredumps e identificar la petición/extensión que falla.
Tarea 3: Buscar evidencia del OOM killer del kernel
cr0x@server:~$ journalctl -k -b | grep -Ei 'oom|out of memory|killed process' | tail -n 20
Dec 29 08:57:02 server kernel: oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/php8.3-fpm.service,task=php-fpm8.3,pid=17602,uid=33
Dec 29 08:57:02 server kernel: Out of memory: Killed process 17602 (php-fpm8.3) total-vm:1884320kB, anon-rss:812344kB, file-rss:0kB, shmem-rss:0kB, UID:33 pgtables:2308kB oom_score_adj:0
Dec 29 08:57:03 server kernel: systemd[1]: php8.3-fpm.service: Main process exited, code=killed, status=9/KILL
Significado: PHP-FPM fue matado por OOM, no “se bloqueó aleatoriamente”. El SEGV en la Tarea 2 podría ser un incidente distinto; no mezcles líneas de tiempo.
Decisión: Tratar como problema de capacidad/concurrencia: reducir pm.max_children, arreglar fugas de memoria o añadir RAM/swap. También verifica si PHP-FPM está en un cgroup con un tope de memoria.
Tarea 4: Identificar qué pool(s) están implicados
cr0x@server:~$ ls -1 /etc/php/8.3/fpm/pool.d/
www.conf
api.conf
worker.conf
Significado: Múltiples pools. Las fallas pueden estar aisladas en un pool con ajustes distintos.
Decisión: Reducir el alcance: encuentra qué pool muestra muertes de child; no asumas que “PHP está caído” si solo un pool está roto.
Tarea 5: Revisar logs de pools de PHP-FPM por muertes de child y peticiones lentas
cr0x@server:~$ grep -R --line-number -E 'SIGSEGV|exited on signal|server reached pm\.max_children|slowlog|terminated' /var/log/php8.3-fpm.log | tail -n 30
14592:WARNING: [pool api] child 19044 exited on signal 11 (SIGSEGV) after 3.221146 seconds from start
14593:NOTICE: [pool api] child 19061 started
14610:WARNING: [pool www] server reached pm.max_children setting (20), consider raising it
14625:WARNING: [pool www] child 19104, script '/var/www/site/index.php' (request: "GET /") executing too slow (5.112 sec), logging
Significado: Dos problemas separados: el pool api está sufriendo segfaults; el pool www está limitado por capacidad y lento.
Decisión: Dividir el trabajo: estabilizar el pool API (extensiones/JIT), luego ajustar la concurrencia/tiempos del pool WWW.
Tarea 6: Confirmar alineación de listen socket y upstream de Nginx
cr0x@server:~$ ss -xlpn | grep -E 'php|fpm'
u_str LISTEN 0 4096 /run/php/php8.3-fpm.sock 32564 * 0 users:(("php-fpm8.3",pid=19212,fd=8))
Significado: El socket existe y está siendo escuchado por PHP-FPM.
Decisión: Si Nginx aún lanza 502, revisa permisos en el socket y el user/group de Nginx; no es un socket faltante.
Tarea 7: Verificar propiedad y modo del socket
cr0x@server:~$ stat -c '%n %U:%G %a' /run/php/php8.3-fpm.sock
/run/php/php8.3-fpm.sock www-data:www-data 660
Significado: El socket es propiedad de www-data y es escribible por el grupo; típico cuando Nginx corre como www-data.
Decisión: Si Nginx corre como nginx, añádelo al grupo del socket o ajusta listen.owner/listen.group/listen.mode en el pool.
Tarea 8: Inspeccionar el error log de Nginx para el error upstream preciso
cr0x@server:~$ tail -n 30 /var/log/nginx/error.log
2025/12/29 09:14:12 [error] 20220#20220: *884 connect() to unix:/run/php/php8.3-fpm.sock failed (111: Connection refused) while connecting to upstream, client: 198.51.100.24, server: example, request: "GET / HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.3-fpm.sock:", host: "example"
2025/12/29 09:14:20 [error] 20220#20220: *901 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 198.51.100.29, server: example, request: "POST /api HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.3-fpm.sock:", host: "example"
Significado: Primer error: PHP-FPM no estaba aceptando (down o reiniciando). Segundo error: PHP-FPM aceptó pero no respondió lo suficientemente rápido.
Decisión: Arregla primero el bucle de crash/reinicio. Los timeouts suelen ser síntoma de falta de workers o I/O bloqueado.
Tarea 9: Comprobar si PHP-FPM tiene límite de memoria por systemd/cgroups
cr0x@server:~$ systemctl show php8.3-fpm -p MemoryMax -p MemoryHigh -p TasksMax -p OOMPolicy
MemoryMax=536870912
MemoryHigh=0
TasksMax=512
OOMPolicy=stop
Significado: Hay un tope de 512 MiB para el servicio. Bajo carga, lo alcanzarás aunque el host tenga RAM libre.
Decisión: Eleva o elimina el tope (con cuidado) y luego revisa el dimensionamiento de pools. Si no lo pusiste tú, audita quién lo hizo.
Tarea 10: Medir memoria por worker para dimensionar pm.max_children sensatamente
cr0x@server:~$ ps -o pid,rss,cmd -C php-fpm8.3 --sort=-rss | head -n 8
PID RSS CMD
19244 148320 php-fpm: pool www
19241 142908 php-fpm: pool www
19262 139440 php-fpm: pool api
19251 136112 php-fpm: pool www
19212 31240 php-fpm: master process (/etc/php/8.3/fpm/php-fpm.conf)
Significado: Los workers tienen ~135–150 MiB RSS cada uno bajo la carga actual. 20 children pueden significar ~3 GB solo de workers, más Opcache y demás.
Decisión: Si tienes 2–4 GB RAM, pm.max_children=20 es una fantasía. Reduce children o la memoria por petición (app/límites), o añade RAM.
Tarea 11: Habilitar y leer el slowlog de PHP-FPM para detectar “se colgó y luego murió”
cr0x@server:~$ sudo grep -nE 'slowlog|request_slowlog_timeout' /etc/php/8.3/fpm/pool.d/www.conf
55:request_slowlog_timeout = 5s
56:slowlog = /var/log/php8.3-fpm-www-slow.log
Significado: Slowlog se dispara a 5s. Es suficientemente agresivo para captar peticiones patológicas sin ahogarte en logs.
Decisión: Si no tienes slowlog activo durante incidentes, estás eligiendo ignorancia. Enciéndelo en los pools afectados y luego reproduce bajo carga.
Tarea 12: Confirmar la versión de PHP y módulos cargados por sospecha de crash
cr0x@server:~$ php-fpm8.3 -v
PHP 8.3.6 (fpm-fcgi) (built: Nov 21 2025 10:14:22)
Copyright (c) The PHP Group
Zend Engine v4.3.6, Copyright (c) Zend Technologies
with Zend OPcache v8.3.6, Copyright (c), by Zend Technologies
cr0x@server:~$ php -m | egrep -i 'opcache|redis|imagick|swoole|xdebug'
imagick
opcache
redis
Significado: Opcache está presente (esperado). Imagick es una fuente frecuente de “crashes nativos” porque enlaza con bibliotecas de ImageMagick; redis también puede tener comportamiento sensible a versiones.
Decisión: Si ves SIGSEGV: deshabilita temporalmente extensiones de alto riesgo en el pool que falla para aislar. El objetivo es hacer que el crash pare y luego reintroducir.
Tarea 13: Comprobar manejo de core dumps (para que los crashes sean accionables)
cr0x@server:~$ coredumpctl list php-fpm8.3 | head
TIME PID UID GID SIG COREFILE EXE
Mon 2025-12-29 09:14:11 UTC 18244 0 0 11 present /usr/sbin/php-fpm8.3
Significado: Existe un core. Eso es oro para depurar crashes nativos.
Decisión: Si puedes, extrae un backtrace en staging con símbolos de depuración; en producción, al menos conserva el core y correlaciónalo con cambios de deploy y módulos habilitados.
Tarea 14: Verificar límites de descriptores de archivo (vector sutil de crash/inestabilidad)
cr0x@server:~$ systemctl show php8.3-fpm -p LimitNOFILE
LimitNOFILE=1024
Significado: 1024 archivos abiertos puede ser demasiado bajo para sitios ocupados (sockets, ficheros, logs, upstreams). Cuando lo alcanzas, obtienes fallos extraños: acepts fallidos, opens fallidos, a veces timeouts en cascada.
Decisión: Aumenta LimitNOFILE en un override de systemd si estás a escala; luego verifica en tiempo de ejecución.
Tarea 15: Revisar denegaciones de AppArmor (a Ubuntu le encanta AppArmor)
cr0x@server:~$ journalctl -k -b | grep -i apparmor | tail -n 10
Dec 29 09:02:41 server kernel: audit: type=1400 apparmor="DENIED" operation="open" profile="/usr/sbin/php-fpm8.3" name="/srv/secrets/api-key.txt" pid=18011 comm="php-fpm8.3" requested_mask="r" denied_mask="r" fsuid=33 ouid=0
Significado: Un proceso de PHP-FPM fue denegado acceso. Eso puede provocar errores fatales, reintentos interminables o fallas en cascada dependiendo del comportamiento de la app.
Decisión: Arregla rutas y permisos o ajusta el perfil de AppArmor. No uses “chmod 777” para cumplir con esto.
Tarea 16: Validar configuración antes de reiniciar (deja de enviar sintaxis rota)
cr0x@server:~$ php-fpm8.3 -t
[29-Dec-2025 09:20:55] NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful
Significado: La sintaxis es OK. No es garantía de semántica correcta, pero evita reinicios humillantes.
Decisión: Haz que php-fpm -t forme parte de pipelines de deploy y hooks previos al reinicio.
Patrones de fallo en Ubuntu 24.04: cómo se ven
Patrón A: OOM kills (el clásico “reinicia, luego muere otra vez”)
Sintomas:
- 502 intermitentes bajo carga, peor durante picos de tráfico o trabajos por lotes.
status=9/KILLen systemd,Out of memory: Killed processen logs del kernel.- Los workers muestran RSS grande; el uso de memoria sube hasta una muerte súbita.
Mecánica:
- Cada worker es un proceso con su propia huella de memoria. Con
pm=dynamic, la concurrencia aumenta con la demanda y la memoria aumenta con ella. - Las peticiones pueden asignar mucha memoria para JSON, hidratación de ORM, procesamiento de imágenes, generación de PDF o simplemente una fuga.
- Si ejecutas todo en una sola VM (web + db + caches) estás creando una pelea por la RAM.
Patrón B: Segfaults (código de salida 139, SIGSEGV)
Sintomas:
- systemd muestra
status=11/SEGVo el log muestra child exited on SIGSEGV. - Frecuentemente ligado a una ruta específica, tipo de subida, operación de imagen o llamada a extensión.
- Puede comenzar después de una actualización: versión menor de PHP, actualización de librerías o recompilación de extensiones.
Mecánica:
- PHP en sí es C. Las extensiones son C. Un segfault significa que alguien tocó memoria que no debía. El userland de PHP puede provocarlo indirectamente.
- Opcache y JIT pueden amplificar bugs raros porque alteran caminos de ejecución y layouts de memoria.
- Paquetes mixtos o módulos .so obsoletos causan incompatibilidades ABI; puede correr un tiempo y luego fallar.
Patrón C: Bind de socket y permisos (no es un crash, pero parece uno)
Sintomas:
- PHP-FPM no arranca: “unable to bind listening socket.”
- Log de Nginx:
connect() ... failed (13: Permission denied).
Mecánica:
- Archivo de socket obsoleto o dirección
listenequivocada. - Pool corriendo bajo un usuario distinto al esperado; Nginx no puede acceder al socket.
- Dos pools intentando enlazar la misma ruta de socket.
Patrón D: “No está caído” — simplemente colgado o saturado
Sintomas:
- Nginx dice upstream timed out; PHP-FPM permanece “active (running).”
- Línea de log:
server reached pm.max_children. - Slowlog muestra la misma función cuello de botella: llamadas DB, llamadas de red, I/O en FS.
Mecánica:
- Se te acabaron los workers. La cola se acumula. Nginx espera. Los clientes se van.
- O tienes workers, pero están bloqueados en algo externo y no vuelven al pool.
Soluciones que realmente funcionan (OOM, segfaults, sockets, límites)
Bucket de soluciones 1: Evitar OOM matando dimensionando la concurrencia a la memoria
La verdad poco glamorosa: la mayoría de los “crashes” de PHP-FPM bajo carga son autoinfligidos por permitir más concurrencia de la que la RAM puede soportar.
1) Reduce pm.max_children según el RSS observado
Usa la Tarea 10 para medir RSS realista durante cargas típicas y pico. Luego haz las cuentas. Si los workers promedian 140 MiB RSS y puedes dedicar 1.5 GiB a workers PHP, estás en ~10 hijos, no 30.
En el archivo de pool:
cr0x@server:~$ sudo grep -nE 'pm\.max_children|pm\.start_servers|pm\.min_spare_servers|pm\.max_spare_servers' /etc/php/8.3/fpm/pool.d/www.conf
90:pm.max_children = 20
91:pm.start_servers = 4
92:pm.min_spare_servers = 2
93:pm.max_spare_servers = 6
Decisión: Baja pm.max_children a un número seguro y observa cola y latencia. Es mejor encolar que morir.
2) Pon un techo de memoria por petición (estratégicamente)
No pongas memory_limit a algo “enorme” porque una página lo necesita. Así le das a cada worker una granada. En su lugar, maneja tareas pesadas de forma asíncrona o aíslalas en un pool dedicado con controles estrictos.
3) Arregla el antipatrón “un pool lo hace todo”
Crea pools separados para:
- Solicitudes web públicas (sensibles a latencia)
- Solicitudes API (a menudo con ráfagas)
- Admin/cron/background (hambrientos de memoria, toleran más latencia)
Esto limita el radio de daño. Una exportación administrativa fuera de control no debería expulsar tu página principal de la memoria.
4) Revisa topes de memoria en systemd si existen
Si MemoryMax está definido (Tarea 9), ajústalo con un override drop-in de systemd. Es un cambio quirúrgico, no un ritual.
cr0x@server:~$ sudo systemctl edit php8.3-fpm
# (editor opens; add the following)
# [Service]
# MemoryMax=0
# LimitNOFILE=65535
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart php8.3-fpm
cr0x@server:~$ systemctl status php8.3-fpm --no-pager
● php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager
Active: active (running) since Mon 2025-12-29 09:28:12 UTC; 3s ago
Decisión: Si quitas topes, compensa con pm.max_children sensato y monitoreo, o solo moverás el fallo a “OOM de toda la VM”.
Bucket de soluciones 2: Detener segfaults aislando al culpable
Los segfaults son deterministas eventualmente. Tu trabajo es reducir el espacio de búsqueda.
1) Desactiva temporalmente JIT si está habilitado
JIT puede ser rápido; también puede amplificar crashes en casos límite. Si no sabes si JIT está encendido, asume que alguien lo activó “por rendimiento” y luego lo olvidó.
cr0x@server:~$ php -i | grep -i opcache.jit
opcache.jit => tracing => tracing
opcache.jit_buffer_size => 128M => 128M
Apágalo (por ahora) en /etc/php/8.3/fpm/conf.d/10-opcache.ini o mediante un override dedicado:
cr0x@server:~$ sudo sed -n '1,120p' /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.jit=tracing
opcache.jit_buffer_size=128M
Decisión: Pon opcache.jit=0 y opcache.jit_buffer_size=0, reinicia y comprueba si los crashes paran. Si paran, encontraste una palanca importante.
2) Deshabilita extensiones de alto riesgo por pool
Puedes usar controles estilo php_admin_value[extension]= indirectamente con valores por pool, pero la carga de módulos suele ser global. Prácticamente, aíslas corriendo una instancia FPM separada o un pool con php.ini distinto cuando sea posible, o desinstalando la extensión brevemente en una ventana de mantenimiento.
Si Imagick está implicada y los crashes correlacionan con operaciones de imagen, prueba removiéndola en staging o deshabilita la bandera de la funcionalidad que la invoca temporalmente.
3) Captura un core dump y obtén un backtrace (el enfoque adulto)
Cuando el crash es caro, deja de adivinar. Usa coredumpctl (Tarea 13) y extrae información. Incluso sin símbolos completos, a menudo puedes ver el nombre del módulo que provoca el fallo.
4) Purga paquetes mixtos / módulos obsoletos tras una actualización
Las actualizaciones de Ubuntu pueden dejar módulos antiguos. Asegúrate de no estar cargando una extensión compilada para otro PHP minor.
cr0x@server:~$ php -i | grep -E '^extension_dir'
extension_dir => /usr/lib/php/20230831 => /usr/lib/php/20230831
Decisión: Verifica que extension_dir coincida con la versión API de PHP instalada y que los módulos en ese directorio provengan del mismo set de compilación/repositorio.
Bucket de soluciones 3: Hacer los sockets y permisos aburridos
Los problemas de socket hacen perder tiempo porque parecen “PHP caído” mientras PHP-FPM cree que está bien.
1) Asegura sockets únicos por pool
Si dos pools escuchan en la misma ruta de socket, uno gana y el otro falla, y tu outage se vuelve una tirada de moneda.
cr0x@server:~$ grep -R --line-number '^listen\s*=' /etc/php/8.3/fpm/pool.d/
/etc/php/8.3/fpm/pool.d/www.conf:34:listen = /run/php/php8.3-fpm.sock
/etc/php/8.3/fpm/pool.d/api.conf:34:listen = /run/php/php8.3-fpm.sock
Decisión: Arregla inmediatamente: da a cada pool su propio socket (o puerto TCP) y actualiza los upstreams de Nginx en consecuencia.
2) Alinea el usuario de Nginx con los permisos del socket
Revisa el user de Nginx:
cr0x@server:~$ grep -nE '^\s*user\s+' /etc/nginx/nginx.conf
1:user www-data;
Decisión: Si Nginx corre como nginx, entonces configura listen.group=nginx (o añade nginx al grupo www-data), y mantiene permisos 660.
Bucket de soluciones 4: Evitar que los bucles de reinicio se conviertan en outages
Los bucles de reinicio de systemd son útiles hasta que no lo son. Si PHP-FPM se cae al instante, systemd seguirá intentando, consumiendo CPU, llenando logs y haciendo la experiencia de Nginx… emocionante.
1) Ralentiza el bucle de reinicio mientras depuras
Añade un override de systemd con RestartSec. Esto te da tiempo y reduce ruido en logs.
cr0x@server:~$ sudo systemctl edit php8.3-fpm
# [Service]
# RestartSec=5s
Decisión: Usa esto como cinturón temporal, no como solución permanente. El objetivo no es “reiniciar más lento”, es “dejar de fallar”.
2) Usa reload cuando sea seguro, restart cuando sea necesario
Reload es más suave; restart es un martillo. Para cambios de config que PHP-FPM pueda recargar, haz:
cr0x@server:~$ sudo systemctl reload php8.3-fpm
cr0x@server:~$ journalctl -u php8.3-fpm -n 5 --no-pager
Dec 29 09:33:10 server systemd[1]: Reloaded php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager.
Decisión: Si sospechas corrupción de memoria o problemas de extensiones, reiniciar es más seguro. Reload no te salvará de un proceso envenenado.
Broma #2: Reiniciar PHP-FPM cada cinco minutos no es “self-healing”. Es “self-soothing”, y no impresiona a quien está de guardia.
Tres microhistorias del mundo corporativo (qué falla realmente)
Microhistoria 1: El incidente causado por una suposición equivocada
Migraron una pila de e-commerce mediana a Ubuntu 24.04 durante un fin de semana. El plan de cambios era sólido: nuevas AMIs, blue/green, canary de tráfico. El lunes por la mañana el canary estaba bien. Al mediodía, los tickets de soporte se dispararon: checkout esporádicos fallando con 502s.
El ingeniero de guardia asumió “inestabilidad de red” porque los errores eran intermitentes y agrupados alrededor de una llamada a la API de pagos. Persiguieron TLS upstream, caching de DNS e incluso conntrack del firewall. Eran inteligentes, estaban ocupados y estaban equivocados.
La pista real estaba en el journal del kernel: OOM kills contra php-fpm. La nueva imagen base tenía un drop-in de systemd con MemoryMax para “hardening”. Se copió de una plantilla de servicio más pequeño donde tenía sentido. Aquí fue una trampa. Bajo tráfico real, un puñado de peticiones grandes al carrito empujaron a los workers PHP al techo del cgroup.
Cuando dejaron de asumir “red”, la solución tomó una hora: subir el tope de memoria, reducir pm.max_children según RSS medido y sacar el pool admin usado para imports. También añadieron una alerta en eventos OOM kill. No porque sea elegante. Porque es la línea de registro que termina las discusiones.
La parte interesante no fue la solución; fue el modo de fallo. Los 502 intermitentes pueden ser pura capacidad. Tu intuición intentará culpar a la red. El kernel te dirá silenciosamente que es memoria.
Microhistoria 2: La optimización que salió mal
Un equipo distinto tenía un problema de latencia. Las páginas estaban lentas después de una actualización. Alguien propuso habilitar Opcache JIT porque había leído que mejora cargas intensivas de CPU. Lo activaron globalmente, aumentaron el buffer y declararon victoria tras un benchmark rápido.
Dos días después, comenzaron segfaults aleatorios en workers PHP-FPM. No constantes, no reproducibles inmediatamente. Lo suficiente para desencadenar reinicios en horas pico, lo que se convirtió en errores visibles para clientes. Los logs mostraban child exited on signal 11. Esa es la clase de línea que te hace mirar al techo por un minuto.
Hicieron lo correcto después: revirtieron JIT y los crashes pararon. Eso no probó que JIT fuera “malo”. Probó que el cambio interactuó con su conjunto particular de extensiones y mezcla de peticiones de una manera que el benchmark nunca ejercitó. El benchmark tocó la ruta feliz. Producción siempre toca la ruta rara.
El postmortem fue directo: las características de rendimiento son cambios, no comida gratis. Introdujeron una política: habilitar JIT solo por pool, detrás de un despliegue controlado y con monitoreo de tasa de crashes. Lo gracioso es que la política fue más barata que el incidente. Suele pasar así.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día
Una compañía SaaS ejecutaba múltiples pools PHP: www para tráfico interactivo, api para partners y jobs para tareas background. Cada pool tenía su propio socket, su propio slowlog y timeouts estrictos. No era glamuroso. También fue la razón por la cual su incidente fue solo molestia leve.
Una tarde, un job background empezó a generar PDFs enormes debido a un cambio de plantilla defectuoso. El uso de memoria por petición se duplicó. El pool jobs comenzó a alcanzar su propio límite de pm.max_children y a encolar. Los workers giraban. Pero el sitio público se mantuvo arriba.
Porque el radio de daño estaba contenido, quien estaba de guardia pudo depurar sin un fuego visible para clientes. El slowlog capturó pilas que mostraban la ruta del generador de PDF. Revirtieron la plantilla, limpiaron la cola de jobs y luego redujeron el límite de memoria para ese pool para que fallara rápido la próxima vez.
La lección no fue “sé ingenioso”. Fue “estate particionado”. Cuando PHP-FPM falla, el aislamiento es tu amortiguador. Pools separados son disyuntores operacionales y casi no cuestan nada comparado con el downtime.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: Nginx muestra 502; PHP-FPM está “active (running)”
Causa raíz: Ruta de socket equivocada en Nginx, mismatch de permisos en el socket o Nginx apuntando a un pool que no está escuchando.
Solución: Usa ss -xlpn para confirmar el socket exacto; verifica que el upstream de Nginx coincida; comprueba owner/mode con stat; alinea listen.owner/listen.group/listen.mode.
2) Síntoma: PHP-FPM sigue reiniciándose; systemd muestra status=9/KILL
Causa raíz: OOM killer del kernel o enforcement de cgroup MemoryMax.
Solución: Revisa journalctl -k para líneas OOM; inspecciona con systemctl show topes de memoria; reduce pm.max_children; arregla endpoints que consumen mucha memoria; añade RAM si hace falta.
3) Síntoma: Código de salida 139 / status=11/SEGV
Causa raíz: Crash nativo en PHP o en una extensión, frecuentemente ligado a actualizaciones de librerías, Opcache/JIT, Imagick o incompatibilidades ABI.
Solución: Desactiva JIT; elimina/deshabilita extensiones sospechosas; captura core con coredumpctl; asegúrate de un set de paquetes consistente; reproduce en staging con los mismos binarios.
4) Síntoma: “server reached pm.max_children” y latencia en aumento
Causa raíz: No hay suficientes workers para la concurrencia de peticiones, o los workers están bloqueados en I/O (DB, red, filesystem) y no regresan al pool.
Solución: Usa slowlog para encontrar llamadas bloqueantes; arregla índices DB o llamadas externas; ajusta pm.max_children solo después de confirmar margen de memoria; considera caching y jobs asíncronos.
5) Síntoma: PHP-FPM no arranca después de una actualización
Causa raíz: Error de configuración de pool, sockets listen duplicados o archivo pid/socket obsoleto.
Solución: Ejecuta php-fpm -t; grep en archivos de pool por listen duplicados; elimina socket obsoleto si es necesario; reinicia limpio.
6) Síntoma: “Permission denied” aleatorio sobre ficheros que “deberían ser legibles”
Causa raíz: Denegación de AppArmor o user/group incorrecto en el pool.
Solución: Revisa logs de auditoría del kernel para denegaciones de AppArmor; guarda secretos en ubicaciones aprobadas; ajusta el perfil o la ruta; no amplíes permisos indiscriminadamente.
7) Síntoma: Funciona durante horas, luego empieza a fallar hasta reiniciar
Causa raíz: Fuga de memoria, fuga de descriptores de archivo o fragmentación/presión de Opcache bajo patrones de tráfico específicos.
Solución: Rastrea crecimiento de RSS por worker; incrementa LimitNOFILE si hace falta; añade reloads suaves periódicos solo si entiendes la fuga; arregla la causa raíz en la app/extensión.
Listas de verificación / plan paso a paso
Checklist A: Triage en producción (15 minutos)
- Ejecuta
systemctl status php8.3-fpm. Anota el estado de salida/señal. - Extrae
journalctl -u php8.3-fpm -b. Encuentra la línea conSEGV,KILLo errores de bind. - Busca en logs del kernel OOM:
journalctl -k -b | grep -Ei 'oom|killed process'. - Revisa el error log de Nginx para el tipo de error upstream (refused vs timeout vs permission).
- Confirma existencia y permisos del socket:
ss -xlpn,stat. - Identifica qué pool falla (nombre del pool en logs de PHP-FPM).
Checklist B: Estabilizar (mismo día)
- Si OOM: reduce
pm.max_childreninmediatamente y aísla trabajos pesados en un pool separado. - Si SEGV: deshabilita JIT (si está) y quita extensiones de alto riesgo temporalmente; preserva core dumps.
- Si saturación/timeout: habilita slowlog, pon
request_slowlog_timeouty confirma quepm.max_childrenno sea demasiado bajo. - Establece
request_terminate_timeoutrealista por pool para evitar colgados infinitos. - Eleva
LimitNOFILEsi te acercas a límites de FD.
Checklist C: Evitar que vuelva a ocurrir (esta semana)
- Añade alertas sobre eventos OOM del kernel que afecten a PHP-FPM.
- Monitorea métricas de pools PHP-FPM: procesos activos, cola de escucha, max children alcanzados, percentiles de duración de peticiones.
- Documenta intención y presupuestos de recursos por pool: web vs api vs jobs.
- Prueba upgrades con patrones de tráfico representativos, no solo un curl a la home.
- Haz obligatoria la validación de configuración (
php-fpm -t) en deploy.
FAQ
¿Por qué Ubuntu 24.04 hace que PHP-FPM se bloquee “más” que antes?
Normalmente no es que Ubuntu “lo cause”; las actualizaciones cambian versiones de PHP, librerías enlazadas y valores por defecto de systemd. Eso puede exponer bugs de extensiones, topes de memoria o cambios en características de rendimiento.
¿Qué log debo leer primero: Nginx, PHP-FPM o systemd?
Empieza con systemd/journal (journalctl -u php8.3-fpm) y los logs del kernel para OOM. Nginx te dice el síntoma; systemd/kernel te dice la causa.
¿Qué significa “status=11/SEGV”?
El proceso murió con un fallo de segmentación (SIGSEGV). Trátalo como un crash nativo: sospecha extensiones, Opcache/JIT, incompatibilidades ABI o librerías.
¿Qué significa “server reached pm.max_children” y debería simplemente aumentarlo?
Significa que todos los workers estaban ocupados y las nuevas peticiones quedaron en cola. Aumentarlo puede ayudar si tienes margen de memoria y los workers no están bloqueados. Si ya estás cerca de límites de memoria, aumentarlo convertirá latencia en un OOM kill.
¿Cómo dimensiono pm.max_children correctamente?
Mide el RSS de los workers bajo carga real (ps ordenado por RSS), decide cuánta RAM puedes dedicar a workers PHP, luego divide. Deja buffer para OS, Nginx, caches y picos.
¿Es el swap una solución válida para OOM de PHP-FPM?
Swap puede evitar OOMs inmediatos, pero también puede convertir tu servidor en una máquina de latencia. Usa swap como red de seguridad, no como plan de capacidad. Si el swap crece en tráfico normal, estás subdimensionado o mal configurado.
¿Una sola petición mala puede tumbar todos los workers de PHP-FPM?
Sí. Una petición que provoca un segfault en una ruta de extensión compartida puede matar repetidamente workers. Además, problemas con estado compartido (Opcache en memoria compartida) pueden crear fallos correlacionados entre workers.
¿Debería usar TCP en lugar de sockets Unix para dejar de recibir 502s?
No. TCP vs socket rara vez es la causa raíz. Usa TCP si necesitas separación de red o límites de contenedor; de lo contrario, arregla permisos y la configuración de listen.
¿Cuál es la forma más rápida de atrapar peticiones lentas o colgadas de PHP?
Habilita request_slowlog_timeout y slowlog por pool, y fija timeouts de terminación sensatos. Slowlog te da trazas de pila para “¿por qué está atascado?” sin adivinar.
¿Puede systemd en sí estar matando PHP-FPM?
Sí—vía límites de recursos (MemoryMax, TasksMax), watchdog/timeouts en setups inusuales o políticas de reinicio agresivas que interactúan con config que falla. Inspecciona con systemctl show.
Conclusión: próximos pasos que debes hacer hoy
Si PHP-FPM se está bloqueando en Ubuntu 24.04, deja de tratarlo como el clima. Encuentra la línea que nombre al asesino: OOM del kernel, señal de systemd o salida de child de PHP-FPM. Esa única línea te dice qué caja de herramientas abrir.
Haz estos pasos en orden:
- Captura
systemctl statusyjournalctl -u; anota el estado de salida/señal. - Revisa logs del kernel por OOM kills; si están presentes, arregla concurrencia vs memoria inmediatamente.
- Si SEGV, desactiva JIT (si está), aísla extensiones y preserva core dumps.
- Separa pools por carga de trabajo y da a cada pool su socket y presupuesto de recursos.
- Activa slowlog en el pool que duele, luego arregla lo que indique (DB, llamadas externas, filesystem).
Una vez hecho eso, PHP-FPM se vuelve lo que debería ser: infraestructura aburrida. El mejor tipo.