Alguien intenta subir un PDF de 40 MB. La interfaz gira, luego muere con un orgulloso “413” o un vago “The uploaded file exceeds the upload_max_filesize directive.” Subes upload_max_filesize a 128M, recargas algo y… nada cambia. Clásico.
En Ubuntu 24.04, el problema rara vez es “PHP tiene un límite”. El problema es “cambiaste el límite equivocado, en el lugar equivocado, para la SAPI equivocada, detrás de un proxy que no recibió el memo.” Vamos a arreglarlo donde realmente importa.
El modelo mental: las subidas son una carrera de relevos
La subida de archivos no es “algo de PHP”. Es una petición HTTP con un body. Ese body pasa por una cadena:
- Cliente (navegador/aplicación móvil) crea una petición multipart/form-data.
- Cualquier edge/WAF/CDN/reverse proxy aplica su propio tamaño máximo de petición y timeouts.
- Tu balanceador de carga o ingress hace lo mismo.
- Tu servidor web (Nginx/Apache) decide si bufferiza, transmite o rechaza el body.
- PHP-FPM recibe la petición, escribe archivos temporales de la subida, y luego la app los lee/mueve.
- Tu app/framework puede imponer su propia validación de límite y rechazarlo igualmente.
- El almacenamiento (cuota de disco, permisos, sistema de archivos lleno) puede sabotear la etapa de archivos temporales.
El límite que necesitas cambiar es el primer eslabón en la cadena que esté por debajo de tu objetivo. Cambiar uno posterior es como ensanchar una salida de autopista mientras el puente anterior sigue siendo de un solo carril.
Broma #1: Subir upload_max_filesize sin comprobar Nginx es como comprarte una maleta más grande para un vuelo solo con equipaje de mano. Igual te detendrá el agente de la puerta.
Hechos interesantes y contexto histórico
- Los límites de subida de PHP se diseñaron pensando en hosting compartido. Las primeras implantaciones de PHP asumían muchos usuarios no relacionados compartiendo una máquina; valores conservadores ayudaban a evitar que “una subida mate la caja”.
post_max_sizeexiste porque las subidas son parte del cuerpo POST. Las subidas multipart siguen siendo datos de POST; PHP cuenta todo el body de la petición, no solo el archivo.- Nginx históricamente tenía por defecto 1 MB para cuerpos de petición. Ese valor por defecto ha arruinado más lanzamientos de viernes de los que la gente admite.
- Apache tiene múltiples perillas según los módulos. La historia del límite del body cambia con la configuración core, mod_security y los módulos proxy.
- 413 no es un “error de PHP”. Es un estado HTTP que significa que el servidor (o proxy) rechaza la entidad/payload de la petición; PHP puede que ni la vea.
- Las subidas en PHP tocan disco antes de que tu código las vea. El archivo cae en
upload_tmp_dir(o temp del sistema) primero; si ese filesystem está lleno obtendrás fallos misteriosos. - PHP-FPM introdujo overrides a nivel de pool para soportar multi-tenant. Eso es genial hasta que olvidas que un pool tiene
php_admin_valueque anula silenciosamente los ini. - systemd cambió los hábitos de “recarga” de muchos administradores. En Ubuntu moderno, “reiniciar el daemon” es fiable; “recargar la configuración” depende del servicio y de tu paciencia.
- Navegadores y proxies tienen sus propios timeouts. Los límites de tamaño son obvios; los timeouts son sigilosos. Una conexión lenta puede convertir una subida de 100 MB en una fiesta de timeouts.
Guía rápida de diagnóstico
Primero: identifica quién está rechazando la petición
- Si el cliente recibe 413 al instante, el rechazo casi seguro es Nginx/Apache/proxy/WAF. PHP ni siquiera fue invitado.
- Si obtienes una advertencia de PHP sobre
upload_max_filesize, PHP vio las cabeceras de la petición y decidió que el body es demasiado grande. - Si recibes un error genérico de la app después de esperar, sospecha de timeouts, disco temporal lleno o validación de la app.
Segundo: confirma qué SAPI y qué configuración de PHP estás cambiando
- La configuración de PHP CLI es irrelevante para subidas web a menos que ejecutes subidas por CLI (no lo haces).
- Para la web: estás en PHP-FPM (común) o Apache mod_php (menos común en Ubuntu 24.04).
- Confirma la ruta del ini activa y luego confirma overrides de pool.
Tercero: encuentra el límite más pequeño en la cadena
- Límites del edge/WAF/CDN (capas de tamaño del body) y reglas de ingress.
- Nginx:
client_max_body_size; buffering de proxy; timeouts. - Apache: límite del body y reglas de seguridad.
- PHP:
upload_max_filesize,post_max_size,memory_limit, timeouts, directorio temporal. - App: reglas de validación y defaults del framework.
- Disco: espacio libre + inodos + permisos.
Tareas prácticas (comandos, salidas, decisiones)
Estas son las cosas que realmente ejecuto en Ubuntu 24.04 cuando las subidas fallan. Cada tarea incluye: comando, qué significa la salida y la decisión que tomas.
Task 1: Verifica la pila web (Nginx vs Apache) en el host
cr0x@server:~$ systemctl status nginx apache2 --no-pager
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-12-30 09:12:31 UTC; 2h 7min ago
● apache2.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/apache2.service; disabled; preset: enabled)
Active: inactive (dead)
Significado: Nginx está en juego; Apache no. Si estás editando la configuración de Apache, estás decorando el edificio equivocado.
Decisión: Concéntrate en los límites de Nginx y PHP-FPM. Ignora Apache.
Task 2: Confirma que se usa PHP-FPM y qué versión
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: active (running) since Mon 2025-12-30 09:10:07 UTC; 2h 10min ago
Significado: PHP 8.3 FPM sirve PHP web. Ubuntu 24.04 incluye PHP 8.3 por defecto, así que esto cuadra.
Decisión: Todos los ajustes de subida de PHP deben aplicarse al config de FPM, no al CLI.
Task 3: Encuentra el php.ini activo para PHP-FPM (no CLI)
cr0x@server:~$ php-fpm8.3 -i | grep -E 'Loaded Configuration File|Scan this dir for additional .ini files|upload_max_filesize|post_max_size' | head
Loaded Configuration File => /etc/php/8.3/fpm/php.ini
Scan this dir for additional .ini files => /etc/php/8.3/fpm/conf.d
upload_max_filesize => 2M => 2M
post_max_size => 8M => 8M
Significado: La SAPI FPM carga /etc/php/8.3/fpm/php.ini. Los valores por defecto son pequeños (2M/8M), por eso tu PDF está llorando.
Decisión: Edita el ini de FPM (o un override en conf.d) y sube también post_max_size junto con upload_max_filesize.
Task 4: Confirma que los valores del CLI son irrelevantes (pero útiles para evitar confusión)
cr0x@server:~$ php -i | grep -E 'Loaded Configuration File|upload_max_filesize|post_max_size' | head
Loaded Configuration File => /etc/php/8.3/cli/php.ini
upload_max_filesize => 128M => 128M
post_max_size => 128M => 128M
Significado: El CLI ya tiene 128M, pero FPM aún está en 2M/8M. Esta es la situación más común de “juro que lo cambié”.
Decisión: Deja de editar el ini del CLI para problemas web. Arregla FPM.
Task 5: Busca overrides a nivel de pool que ganan en silencio
cr0x@server:~$ grep -R --line-number -E 'php_(admin_)?value\[(upload_max_filesize|post_max_size)\]' /etc/php/8.3/fpm/pool.d
/etc/php/8.3/fpm/pool.d/www.conf:402:php_admin_value[upload_max_filesize] = 10M
/etc/php/8.3/fpm/pool.d/www.conf:403:php_admin_value[post_max_size] = 10M
Significado: Incluso si pones 128M en php.ini, el pool fuerza 10M y pierdes. php_admin_value no es negociable en tiempo de ejecución.
Decisión: Actualiza el archivo del pool o quita el override. Luego reinicia FPM.
Task 6: Aplica valores PHP sensatos (editar) y valida la sintaxis
cr0x@server:~$ sudo sed -i 's/^upload_max_filesize = .*/upload_max_filesize = 128M/; s/^post_max_size = .*/post_max_size = 128M/' /etc/php/8.3/fpm/php.ini
cr0x@server:~$ sudo php-fpm8.3 -tt
[30-Dec-2025 11:22:14] NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful
Significado: La config se parsea. Si no lo hace, no reinicies; arréglalo primero.
Decisión: Procede a reiniciar PHP-FPM.
Task 7: Reinicia PHP-FPM (no “recargar y esperar”)
cr0x@server:~$ sudo systemctl restart php8.3-fpm
cr0x@server:~$ systemctl is-active php8.3-fpm
active
Significado: PHP-FPM volvió arriba. Reiniciar es contundente, pero determinista.
Decisión: Ahora verifica los valores efectivos del servicio en ejecución.
Task 8: Confirma valores efectivos vía una petición FastCGI local (sin conjeturas del navegador)
cr0x@server:~$ printf '%s\n' '<?php echo ini_get("upload_max_filesize"),"\n",ini_get("post_max_size"),"\n";' | sudo tee /var/www/html/_ini.php > /dev/null
cr0x@server:~$ curl -sS http://127.0.0.1/_ini.php
128M
128M
Significado: El PHP servido por la web ahora informa 128M/128M. Esa es la verdad que importa.
Decisión: Si las subidas siguen fallando, el cuello de botella está aguas arriba (Nginx/proxy) o aguas abajo (disco temporal, app).
Task 9: Comprueba el límite de body de petición en Nginx
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -R --line-number 'client_max_body_size' -n /dev/stdin | head
# (no output)
Significado: No se encontró una configuración explícita. Nginx usará su valor por defecto, que suele ser 1m.
Decisión: Establece client_max_body_size en el contexto correcto (server o location) y recarga Nginx.
Task 10: Añade client_max_body_size y valida la config de Nginx
cr0x@server:~$ sudo tee /etc/nginx/conf.d/uploads.conf > /dev/null <<'EOF'
server {
listen 80 default_server;
server_name _;
client_max_body_size 128m;
}
EOF
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ systemctl is-active nginx
active
Significado: Nginx aceptará bodies de hasta 128 MB para este server. La config se cargó.
Decisión: Vuelve a probar las subidas. Si tu vhost de producción no es el server por defecto, pon la directiva en el vhost real, no en este ejemplo.
Task 11: Detecta un límite en proxy/CDN reproduciendo y observando códigos de estado
cr0x@server:~$ curl -sS -o /dev/null -w '%{http_code}\n' -F 'file=@/var/log/syslog' http://127.0.0.1/upload.php
200
Significado: Localmente está bien. Si la misma petición a través del hostname público devuelve 413, tu edge/proxy es el limitador.
Decisión: Compara la ruta local vs pública. Si local funciona y público falla, deja de tocar PHP y arregla el proxy/WAF/ingress.
Task 12: Inspecciona logs de la capa que se queja
cr0x@server:~$ sudo tail -n 30 /var/log/nginx/error.log
2025/12/30 11:40:01 [error] 24177#24177: *812 client intended to send too large body: 187654321 bytes, client: 203.0.113.44, server: example, request: "POST /upload.php HTTP/1.1", host: "example"
Significado: Nginx rechazó el body. Esa línea de error es básicamente una confesión.
Decisión: Arregla el scope de la configuración en Nginx (vhost/location equivocados), recarga y reintenta.
Task 13: Revisa logs de PHP-FPM por “file too large” vs “no temp dir” vs timeouts
cr0x@server:~$ sudo tail -n 50 /var/log/php8.3-fpm.log
[30-Dec-2025 11:42:18] WARNING: [pool www] child 30112 said into stderr: "PHP Warning: POST Content-Length of 187654321 bytes exceeds the limit of 134217728 bytes in Unknown on line 0"
Significado: Esto es PHP aplicando post_max_size. Cuenta todo el body. Tu “128M” puede quedar justo una vez que añades overhead multipart.
Decisión: Ajusta post_max_size más alto que upload_max_filesize (práctica común: +10–20%).
Task 14: Comprueba espacio temporal y salud de inodos
cr0x@server:~$ df -h /tmp /var/tmp /var/lib/php/sessions
Filesystem Size Used Avail Use% Mounted on
tmpfs 3.1G 3.0G 40M 99% /tmp
/dev/sda2 80G 52G 25G 68% /
Significado: /tmp es tmpfs y está casi lleno. PHP puede escribir archivos temporales ahí (dependiendo de upload_tmp_dir), y “99%” coquetea con el fallo.
Decisión: Libera espacio o mueve upload_tmp_dir a un filesystem real con margen.
Task 15: Verifica cuál es el directorio temporal según PHP
cr0x@server:~$ curl -sS http://127.0.0.1/_ini.php | cat
128M
128M
cr0x@server:~$ php-fpm8.3 -i | grep -E '^upload_tmp_dir|^sys_temp_dir' | head
upload_tmp_dir => no value => no value
sys_temp_dir => no value => no value
Significado: Sin un valor explícito, PHP usa el temp del sistema (a menudo /tmp). Si /tmp es un tmpfs limitado, obtendrás fallos de subida impredecibles.
Decisión: Define upload_tmp_dir a un directorio en disco con permisos correctos.
Task 16: Define upload_tmp_dir y verifica permisos
cr0x@server:~$ sudo install -d -o www-data -g www-data -m 1733 /var/tmp/php-uploads
cr0x@server:~$ sudo grep -n '^upload_tmp_dir' /etc/php/8.3/fpm/php.ini || true
cr0x@server:~$ echo 'upload_tmp_dir = /var/tmp/php-uploads' | sudo tee -a /etc/php/8.3/fpm/php.ini > /dev/null
cr0x@server:~$ sudo systemctl restart php8.3-fpm
cr0x@server:~$ sudo -u www-data bash -lc 'touch /var/tmp/php-uploads/.permtest && ls -la /var/tmp/php-uploads/.permtest'
-rw-r--r-- 1 www-data www-data 0 Dec 30 11:55 /var/tmp/php-uploads/.permtest
Significado: El directorio existe, tiene permisos tipo sticky bit (1733), y el usuario FPM puede escribir ahí.
Decisión: Esto elimina el cuello de botella del tmpfs. Si los fallos persisten, mira timeouts o validación de la app.
De dónde vienen realmente los valores de PHP (precedencia que duele)
Cuando alguien dice “lo puse en php.ini”, mi primera pregunta es: “¿Qué php.ini, para qué SAPI, y algo lo sobreescribió?” La configuración de PHP es en capas, y Ubuntu facilita cambiar la capa equivocada.
El orden de poder (de “gana más” a “gana menos”)
- Configuración del pool PHP-FPM vía
php_admin_value[]yphp_value[]en/etc/php/8.3/fpm/pool.d/*.conf. - Configuración por directorio (solo en algunas SAPIs):
.user.inipara PHP-FPM,.htaccesspara Apache mod_php. (Sí, puedes tener ambos en una migración enmarañada.) - Archivos ini adicionales en
/etc/php/8.3/fpm/conf.d/cargados en orden numérico. - php.ini principal para esa SAPI:
/etc/php/8.3/fpm/php.ini. - Valores compilados por defecto (los valores que obtienes si no configuras nada, que es lo que staging a veces hace sin querer).
Por qué php_admin_value es una trampa
php_admin_value es intencionalmente “solo administrador”. No puede ser sobrescrito por código de la aplicación, y anulará tus ajustes ini. Eso es fantástico para hosting multi-tenant y para evitar que una sola app se convierta en un devorador de memoria. También es la razón por la que terminas con un pool atascado en 10M mientras todos discuten por qué los cambios en php.ini “no funcionan”.
Qué hacer en Ubuntu 24.04
Elige una estrategia y sé consistente:
- Servidor de aplicación único: pon los ajustes globales de subida en
/etc/php/8.3/fpm/php.inio en un/etc/php/8.3/fpm/conf.d/99-uploads.inidedicado. Evita overrides de pool salvo que tengas una razón. - Host multi-app: mantén php.ini conservador y establece límites por pool en cada archivo de
pool.d. Documenta los cambios. En serio. El tú del futuro lo olvidará.
Una cita para tener en mente cuando toques perillas en producción: “La esperanza no es una estrategia.”
— General Gordon R. Sullivan. Se aplica incómodamente bien a “recargué y esperé que funcionara”.
Nginx/Apache/proxies: los otros límites
Las subidas mueren en capas upstream más a menudo de lo que mueren en PHP. No es que PHP sea impecable; es que rechazar un body sobredimensionado temprano es más barato que bufferizarlo y luego decirle a PHP que lo gestione.
Nginx: client_max_body_size y dónde colocarlo
client_max_body_size puede definirse en contexto http, server o location. “Lo puse en nginx.conf” no es información suficiente. Si lo pones en un archivo que no está incluido, o en un server block que no se usa, nada cambia.
Además, si usas múltiples server blocks (redirigir HTTP a HTTPS, vhosts internos/externos separados), debes ponerlo donde se sirva realmente el endpoint de subida.
Apache: los límites de tamaño son una hidra
Si estás en Apache, puedes encontrarte con:
- límites del core de la petición (varía por versión/uso de módulos)
- reglas de mod_security que limitan el tamaño del body o rechazan patrones multipart
- módulos proxy cuando Apache hace front a PHP-FPM
En la práctica, en entornos Apache a menudo el límite vive en el middleware de seguridad más que en Apache en sí.
Reverse proxies y CDNs: el “no” silencioso
Incluso cuando tu origin está correcto, un edge puede negar un body grande:
- Las políticas gestionadas de WAF pueden limitar los bodies de petición.
- Controllers de ingress en plataformas de contenedores frecuentemente tienen caps por defecto.
- Los balanceadores de carga pueden imponer límites de tamaño en headers y bodies.
El síntoma suele ser: curl local a localhost funciona; la subida externa falla con 413 o una página de error del proveedor. Tus logs de origin están limpios porque la petición nunca llegó.
Especificidades de PHP-FPM en Ubuntu 24.04
Las cuatro perillas de PHP que importan para subidas
upload_max_filesize: tamaño máximo de un archivo subido individualmente.post_max_size: tamaño máximo de todo el body POST. Debe ser >= tamaño de la subida más el overhead.memory_limit: no limita directamente el tamaño de la subida, pero muchas apps leen subidas en memoria o las procesan (redimensionar imágenes) y entonces esto se vuelve el techo real.max_input_time/max_execution_time: subidas grandes en conexiones lentas pueden chocar con estos. También vigila timeouts de Nginx/FPM.
¿Qué tamaño deberías poner?
Sé deliberado. “Poner todo a 2G” no es ingeniería; es rendirse.
- Pon
upload_max_filesizesegún la necesidad del producto (ejemplo: 128M). - Pon
post_max_sizeun poco más alto (ejemplo: 140M–160M) porque el multipart añade overhead y los formularios pueden incluir campos. - Pon los topes del servidor web/proxy ligeramente por encima de
post_max_size. - Asegúrate de que el almacenamiento temporal pueda absorber al menos unas pocas subidas concurrentes de ese tamaño.
Alineación de timeouts (la realidad de subidas lentas)
Si tus usuarios suben desde una Wi‑Fi de hotel, la subida puede llevar minutos. Eso es normal. Tu stack debe aceptar esperar ese tiempo.
- Nginx:
client_body_timeout,proxy_read_timeout(si haces proxy), y ajustes de buffering. - PHP-FPM: timeouts de terminación de petición (los ajustes del pool pueden matar “peticiones lentas”).
- App: procesamiento en background vs procesamiento síncrono.
Broma #2: Los límites de subida son el único lugar donde “un megabyte más” puede causar un incidente en producción. Es una dieta para servidores, y ellos también hacen trampa.
Trampas en la capa de la aplicación (frameworks y CMS)
Una vez que el body sobrevive la gauntlet, tu aplicación aún puede rechazar el archivo. Aquí es donde los equipos pierden tiempo porque el mensaje de error parece “problema de servidor” aunque sea lógica de negocio.
Reglas de validación que anulan la infraestructura
Ejemplos que verás:
- Validación de Laravel: reglas
max:en kilobytes. - Restricciones de subida en Symfony fijadas a un valor conservador en un tipo de formulario.
- WordPress: el UI “Maximum upload file size” depende de valores PHP, pero los plugins pueden imponer reglas más estrictas.
- Apps personalizadas: un archivo de configuración limita el tamaño “por seguridad” y nadie lo actualizó.
Explosiones de memoria durante el post-procesado
Tu límite de subida puede ser 128M, pero tu app puede luego:
- leer el archivo entero en memoria (
file_get_contentsen un archivo de 128M es una declaración) - transcodificar o redimensionar imágenes (picos temporales de memoria)
- hashear el archivo en userland de PHP
Ahí es cuando el error real es 500, “Allowed memory size exhausted”, o un worker que muere. La solución no es “aumentar upload_max_filesize otra vez”. La solución es streaming, chunking, o mover el procesamiento fuera del camino de la petición.
Tres micro-historias corporativas desde las minas de uploads
1) Incidente causado por una suposición equivocada: “Cambiamos php.ini, así que está arreglado”
Una plataforma interna necesitaba aceptar adjuntos de facturas más grandes. El equipo actualizó /etc/php/8.3/cli/php.ini en un par de servidores, probó con un script CLI que parseaba archivos y dio por hecho que estaba resuelto. Al día siguiente, la UI web seguía rechazando todo mayor a 10 MB.
Operations recibió alertas porque la tasa de errores subió. Los logs de la app no mostraban nada útil. El canal de incidente se llenó del ruido habitual: “Quizá sea la base de datos”, “Quizá el almacenamiento”, “Quizá el load balancer esté triste”. Nadie quería decir lo obvio: el cambio puede que no se aplicara al runtime que importa.
El problema real fue doble. Primero, PHP-FPM cargaba /etc/php/8.3/fpm/php.ini, que no se había tocado. Segundo, el pool de FPM tenía php_admin_value[post_max_size] en 10M de una antigua hardening sprint “proteger el host”.
La solución tomó minutos una vez que alguien dejó de adivinar: confirma la SAPI, grep para overrides de pool, sube los límites, reinicia FPM y verifica con un endpoint servido por la web. La lección no fue “ten cuidado”. Fue “valida los cambios en la capa que alcanzan a los usuarios, no en la capa que prefieres probar”.
2) Optimización que salió mal: “Buffericemos menos para mejorar el rendimiento”
Un equipo con Nginx delante de PHP-FPM intentó reducir I/O en disco cambiando el comportamiento de buffering de peticiones. La idea tenía sentido: subidas grandes escribían en disco y luego PHP las leía de nuevo. Doble I/O, menos diversión.
Cambiaron settings de buffering y algunos timeouts para “acelerar las cosas”. En pruebas en LAN rápido, todo parecía bien. En producción, usuarios con conexiones lentas vieron fallos intermitentes: las subidas morían alrededor de 30–60 segundos. Mientras tanto, el backend vio picos de conexiones semi-abiertas y workers de FPM esperando.
La optimización falló porque desplazó la presión aguas arriba. En vez de bufferizar cuerpos de forma segura (con uso de recursos predecible), el origin mantuvo conexiones abiertas más tiempo. Los timeouts se convirtieron en el factor limitante y el modelo de concurrencia se volvió feo: menos peticiones completas, más trabajo en vuelo y un bucle donde retries empeoraban todo.
La solución fue aburrida: volver a habilitar buffering sensato para endpoints de subida, poner topes explícitos de tamaño, alinear timeouts con condiciones reales de usuarios e instrumentar contadores de “request aborted”. Más tarde mejoraron el rendimiento, pero cambiando la arquitectura (uploads directos a object storage) en lugar de intentar engañar a la física en el proxy.
3) Práctica aburrida pero correcta que salvó el día: “Demuéstralo con una reproducción local y comprobaciones capa por capa”
Una app regulada empezó a fallar subidas después de una reconstrucción rutinaria del host Ubuntu 24.04. Mismo playbook, mismo Ansible, mismas configs—supuestamente. La mesa de ayuda reportó “subidas >5 MB fallan”, que no es un diagnóstico, es una señal de auxilio.
El ingeniero on-call no tocó configuración al principio. Reprodujo el fallo con un curl local a 127.0.0.1 y vio éxito. Luego repitió la misma petición a través del hostname público y obtuvo un 413. Eso dividió el universo en dos: el origin funciona, el edge rechaza.
Después revisaron los ajustes del ingress controller y encontraron un cap por defecto de tamaño de petición que cambió con una actualización de chart. Nadie lo había fijado porque “los valores por defecto están bien” hasta que no lo están. El ingeniero subió el cap para que coincidiera con los límites de Nginx y PHP del origin, desplegó y verificó con el mismo curl.
Sin heroísmos. Sin refactor nocturno. Solo un enfoque disciplinado: reproducir localmente, comparar rutas, identificar la primera capa que rechaza. Aburrido es bueno. Lo aburrido escala.
Errores comunes (síntomas → causa raíz → solución)
1) Síntoma: 413 “Request Entity Too Large” inmediato
Causa raíz: Nginx/Apache/proxy/WAF rechazó el body antes que PHP.
Solución: Ajusta los topes de body en la capa que rechaza (ej., Nginx client_max_body_size). Verifica con curl local vs público y revisa logs del servidor web por “intended to send too large body”.
2) Síntoma: “exceeds upload_max_filesize directive” en el navegador/app
Causa raíz: El límite de PHP es menor que el tamaño del archivo; a menudo cambiaste el ini del CLI, no el de FPM.
Solución: Confirma la ruta del ini de FPM con php-fpm8.3 -i, actualiza upload_max_filesize y post_max_size, reinicia FPM y valida vía un endpoint web con ini_get.
3) Síntoma: La subida empieza y luego falla tras un tiempo con 504/timeout
Causa raíz: Timeouts en proxy/servidor web/FPM; clientes lentos superan client_body_timeout o timeouts upstream.
Solución: Alinea timeouts entre capas para las duraciones esperadas de subida; considera endpoints dedicados con timeouts más largos o uploads directos a object storage.
4) Síntoma: Fallos aleatorios bajo carga; archivos pequeños funcionan, grandes a veces
Causa raíz: Presión en filesystem temporal (tmpfs lleno), agotamiento de inodos o contención de I/O. PHP escribe archivos temporales primero.
Solución: Monitoriza uso de /tmp, define upload_tmp_dir en un filesystem con espacio y ten en cuenta la concurrencia (N subidas concurrentes * tamaño).
5) Síntoma: Logs de PHP muestran POST Content-Length exceeds limit aunque upload_max_filesize sea alto
Causa raíz: post_max_size es menor que el tamaño total multipart de la petición.
Solución: Fija post_max_size por encima de upload_max_filesize; deja margen para overhead multipart y otros campos.
6) Síntoma: Errores 500 al subir imágenes/videos grandes
Causa raíz: El post-procesado de la app choca con memory_limit o tiempo de ejecución. La subida tuvo éxito; el procesamiento falló.
Solución: Procesado por streaming, encolar trabajo pesado, aumentar memoria cuando esté justificado y restringir tamaños en la app para que coincidan con su capacidad de procesamiento.
7) Síntoma: “Funciona en un servidor pero no en otro”
Causa raíz: Overrides por pool, alcance de vhost distinto, archivos include faltantes o una ruta proxy diferente.
Solución: Compara salidas efectivas de configuración: nginx -T, grep en pools de FPM por php_admin_value, y un volcado ini servido por la web.
Listas de verificación / plan paso a paso
Paso a paso: subir límites de forma segura en Ubuntu 24.04 (Nginx + PHP-FPM)
- Elige un objetivo: por ejemplo, archivo máximo 128M. Decide según necesidades del producto, no por sensaciones.
- Establece límites PHP:
upload_max_filesize = 128Mpost_max_size = 160M(margen)
- Revisa overrides de pool: elimina o sube
php_admin_valueen la config del pool. - Establece límite en Nginx:
client_max_body_size 160m;en el server/location correcto. - Revisa almacenamiento temporal: asegúrate de que el directorio temporal pueda manejar picos de subidas concurrentes.
- Reinicia servicios: reinicia PHP-FPM; recarga Nginx tras
nginx -t. - Verifica desde la ruta web: haz curl al endpoint de la app o a un script diagnóstico.
- Verifica desde la ruta pública: si tienes un edge/proxy, prueba también a través de él.
- Instrumenta y registra: vigila conteos de 413, timeouts upstream y uso de disco temporal.
- Documenta los límites elegidos: anota dónde están establecidos (ini, pool, vhost, proxy). El tú del futuro no es psíquico.
Lista: cuándo NO deberías subir límites
- No puedes justificar quién necesita subidas mayores y por qué.
- No tienes margen de disco para archivos temporales.
- Tu app lee todo el archivo en memoria y no quieres arreglar eso ahora.
- Estás detrás de un edge con un cap duro que no puedes cambiar (solo crearás comportamiento inconsistente).
Lista: guardarraíles operativos
- Establece límites superiores por endpoint. No todos los POST deberían aceptar 160 MB.
- Usa bloques de server/location separados para endpoints de subida con límites/timeouts a medida.
- Alerta sobre la tasa de 413 y la utilización del filesystem temporal.
- Monitorea duraciones de subida (p95/p99). Las subidas lentas son donde nacen bugs de timeout.
Preguntas frecuentes
1) Cambié upload_max_filesize pero phpinfo() sigue mostrando el valor antiguo. ¿Por qué?
Probablemente cambiaste el ini del CLI o el ini de FPM equivocado, o un override de pool está ganando. Verifica con php-fpm8.3 -i y grep en pool.d por php_admin_value.
2) ¿Necesito establecer tanto upload_max_filesize como post_max_size?
Sí. Las subidas multipart son bodies POST. Si post_max_size es menor, PHP rechazará la petición antes de que importe el límite de archivo.
3) ¿Qué valor debería tener post_max_size respecto a upload_max_filesize?
Más grande. Deja margen (10–25% es común) porque los boundaries multipart y campos adicionales aumentan el tamaño total del body.
4) ¿Por qué recibo 413 aunque PHP esté configurado correctamente?
Porque 413 suele emitirlo Nginx/Apache/proxy/WAF. PHP nunca llega a ver la petición. Revisa los logs de la capa que rechaza y su directiva de tamaño de body.
5) ¿memory_limit debe ser mayor que upload_max_filesize?
No estrictamente para la propia subida, porque PHP guarda subidas como archivos temporales. Pero tu aplicación puede leer/procesar el archivo en memoria. Si lo hace, la memoria se vuelve el límite real.
6) ¿Por qué las subidas fallan solo para algunos usuarios?
Conexiones lentas disparan timeouts; proxies corporativos pueden tener sus propios caps; las redes móviles son expertas en volver “debería funcionar” en “qué falla”. Alinea timeouts y prueba con redes simuladas lentas.
7) ¿Puedo establecer límites de subida por sitio en el mismo servidor?
Sí. Usa pools PHP-FPM separados con php_admin_value por pool o bloques de server Nginx separados con distinto client_max_body_size. Sé explícito y documenta.
8) ¿Debería confiar en .user.ini para fijar límites de subida?
Sólo si necesitas control por directorio y aceptas la ambigüedad operativa. La configuración central (pool FPM o ini) es más fácil de auditar y menos sorprendente en incidentes.
9) Mis subidas fallan con “No such file or directory” en PHP. ¿Qué pasa?
Muchas veces es el directorio temporal: ausente, permisos incorrectos o lleno. Define upload_tmp_dir a un directorio escribible por el usuario FPM y asegúrate de que haya espacio.
10) ¿Deberíamos aceptar subidas enormes a través de PHP?
A veces sí (herramientas internas, bajo volumen). Para alto volumen o archivos muy grandes, considera uploads directos a object storage con signed URLs y procesa de forma asíncrona.
Próximos pasos que puedes hacer hoy
- Mide el fallo: reprodúcelo con curl local y a través del hostname público. Identifica si el origin recibe la petición.
- Bloquea la configuración efectiva: confirma valores de FPM vía un endpoint web y confirma la config de Nginx con
nginx -T. - Establece límites alineados: cap del Nginx/proxy ≥ PHP
post_max_size> PHPupload_max_filesize. - Arregla el almacenamiento temporal: evita escribir grandes archivos temporales en un tmpfs casi lleno salvo que disfrutes de fallos intermitentes.
- Decide la arquitectura: si las subidas son críticas y grandes, planifica un patrón direct-to-storage y mantén PHP fuera del camino crítico.
Si haces sólo una cosa: deja de adivinar qué archivo ini está activo y demuéstralo desde el runtime servido por la web. Los fallos de subida prosperan en la ambigüedad. Mata la ambigüedad.