Un sitio web lento puede deberse a la red, al servidor web, a la aplicación, a la base de datos o simplemente a un servidor sin recursos. Cambiar ajustes al azar rara vez funciona: lo eficaz es medir dónde se va el tiempo y corregir solo esa parte. En este tutorial diagnosticarás paso a paso una web servida con Nginx, PHP-FPM y MySQL en Ubuntu 24.04, desde la primera medición con curl hasta una prueba de carga que confirme la mejora.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS que sirva la web con Nginx, PHP-FPM 8.3 y MySQL 8.0 (una pila LEMP típica, por ejemplo WordPress en un VPS de CubePath).
- Un usuario no root con privilegios
sudo. - El dominio del sitio,
your_domainen esta guía, apuntando al servidor. - Otra máquina (tu ordenador u otro servidor) desde la que medir y lanzar la prueba de carga.
Si usas Apache en lugar de Nginx, el razonamiento es idéntico; en el paso 3 se indica el equivalente.
Paso 1: Medir dónde se va el tiempo
Antes de tocar el servidor, mide una petición desde fuera. curl puede desglosar el tiempo de cada fase de la conexión. Ejecútalo desde tu ordenador:
curl -o /dev/null -s -w 'DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\nPrimer byte: %{time_starttransfer}s\nTotal: %{time_total}s\nTamaño: %{size_download} bytes\n' https://your_domain/
DNS: 0.012s
TCP: 0.048s
TLS: 0.121s
Primer byte: 2.874s
Total: 2.931s
Tamaño: 84213 bytes
Los tiempos son acumulados, así que interesa la diferencia entre fases:
- DNS, TCP y TLS altos (cientos de milisegundos cada uno): el problema está en la red o en la distancia al servidor, no en la aplicación.
- Mucho tiempo entre TLS y primer byte (en el ejemplo, más de 2,7 segundos): el servidor tarda en generar la página. Es el caso más habitual y el que cubre el resto de la guía.
- Mucho tiempo entre primer byte y total: la respuesta es muy pesada o la conexión es lenta. Revisa compresión y tamaño de imágenes (paso 6).
Repite la medición con un archivo estático, por ejemplo una imagen o /robots.txt. Si el estático responde en milisegundos y la página tarda segundos, el cuello de botella está en PHP o en la base de datos, no en Nginx ni en la red.
Paso 2: Comprobar los recursos del servidor
Una aplicación correcta también va lenta en un servidor saturado. Conéctate por SSH y empieza por la carga y el número de CPU:
uptime
nproc
10:42:15 up 12 days, 3:10, 1 user, load average: 5.82, 5.40, 4.97
2
Una carga media sostenida por encima del número de CPU (aquí 5,8 con 2 CPU) indica que hay más trabajo del que el servidor puede atender. vmstat muestra de qué tipo es:
vmstat 2 5
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
r b swpd free buff cache si so bi bo in cs us sy id wa st
6 0 512000 48120 10240 301220 120 340 95 410 1820 2950 78 12 2 6 2
Fíjate en estas columnas:
rmayor que el número de CPU yusalto: la CPU está saturada. Busca el proceso responsable contop(pulsaPpara ordenar por CPU).siysodistintos de cero de forma continua: el sistema está usando swap porque falta memoria. Esto por sí solo multiplica los tiempos de respuesta.waalto (más de 10-20): los procesos esperan al disco.stalto: tiempo robado por el hipervisor a una máquina virtual. Si es persistente, el plan del VPS se queda corto para la carga.
Comprueba la memoria disponible y qué procesos la consumen:
free -h
ps -eo pid,user,rss,comm --sort=-rss | head -n 10
Si wa es alto, instala sysstat para ver qué disco está ocupado y cuánto:
sudo apt install sysstat
iostat -xz 2 3
Una columna %util cercana al 100 % o un r_await/w_await de decenas de milisegundos confirma que el disco es el cuello de botella. En una web suele deberse a MySQL sin memoria suficiente para su caché (paso 5).
Paso 3: Registrar el tiempo de cada petición en Nginx
El registro de acceso por defecto de Nginx no incluye cuánto tarda cada petición. Añade un formato que lo registre. Crea un archivo en conf.d, que Nginx carga dentro del bloque http:
sudo nano /etc/nginx/conf.d/timing-log.conf
log_format timing '$request_time $upstream_response_time $status '
'"$request" $remote_addr "$http_user_agent"';
$request_time es el tiempo total que Nginx dedicó a la petición y $upstream_response_time el que tardó PHP-FPM en responder. Ahora usa ese formato en el bloque server de tu sitio, junto a la línea access_log existente:
sudo nano /etc/nginx/sites-available/your_domain
access_log /var/log/nginx/your_domain.timing.log timing;
Comprueba la sintaxis y recarga:
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Deja pasar tráfico real durante un rato y lista las peticiones más lentas. Como el tiempo es el primer campo, basta con ordenar:
sudo sort -rn /var/log/nginx/your_domain.timing.log | head -n 15
8.412 8.410 200 "GET /tienda/?filtro=precio&orden=asc HTTP/2.0" 198.51.100.24 "Mozilla/5.0 ..."
6.905 6.903 200 "POST /wp-admin/admin-ajax.php HTTP/2.0" 203.0.113.50 "Mozilla/5.0 ..."
0.004 - 200 "GET /wp-content/uploads/logo.png HTTP/2.0" 198.51.100.24 "Mozilla/5.0 ..."
Si ambos tiempos son casi iguales, el tiempo se consume en PHP o en la base de datos. Agrupa por URL para saber qué rutas son lentas de forma sistemática y no por un pico puntual:
sudo awk '$2 != "-" {split($5, u, "?"); t[u[1]] += $1; n[u[1]]++} END {for (k in t) printf "%.3f s %5d peticiones %s\n", t[k]/n[k], n[k], k}' /var/log/nginx/your_domain.timing.log | sort -rn | head -n 10
Revisa también cuántas peticiones recibe el sitio por IP. Un único cliente con miles de peticiones (un bot o un rastreador agresivo) puede ser la verdadera causa:
sudo awk '{print $7}' /var/log/nginx/your_domain.timing.log | sort | uniq -c | sort -rn | head -n 10
Notaen Apache el equivalente es añadir
%D(microsegundos) a unLogFormaten/etc/apache2/apache2.confy usarlo en elCustomLogdel sitio.
Paso 4: Analizar PHP-FPM
Comprobar si faltan procesos
PHP-FPM atiende cada petición con un proceso del pool. Cuando todos están ocupados, las nuevas peticiones esperan en cola y la web se ralentiza aunque la CPU no esté al máximo. PHP-FPM lo avisa en su registro:
sudo grep -i 'max_children' /var/log/php8.3-fpm.log | tail -n 5
[25-Sep-2026 10:31:02] WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
El valor por defecto en Ubuntu es pm.max_children = 5, escaso para casi cualquier sitio con tráfico. Para calcular cuántos procesos caben, mide la memoria media de cada uno:
ps -C php-fpm8.3 -o rss= | awk '{s += $1; n++} END {printf "%d procesos, media %.0f MB\n", n, s/n/1024}'
6 procesos, media 62 MB
Divide la memoria que puedes dedicar a PHP entre esa media. Por ejemplo, en un servidor de 4 GB en el que MySQL y el sistema necesitan 1,5 GB, quedan unos 2,5 GB: 2500 / 62 ≈ 40 procesos. Edita el pool:
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 40
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
No subas pm.max_children por encima de lo que cabe en memoria: si el servidor empieza a usar swap, la web irá más lenta que antes.
Activar el registro de peticiones lentas
El slowlog de PHP-FPM guarda la traza de pila de cualquier script que supere un tiempo dado, lo que señala la función exacta que tarda. En el mismo archivo www.conf, añade o descomenta:
slowlog = /var/log/php8.3-fpm.slow.log
request_slowlog_timeout = 3s
Comprueba la configuración y reinicia PHP-FPM:
sudo php-fpm8.3 -t
sudo systemctl restart php8.3-fpm
[25-Sep-2026 10:45:12] NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful
Cuando se produzca una petición lenta, el registro mostrará algo parecido a esto:
sudo tail -n 20 /var/log/php8.3-fpm.slow.log
[25-Sep-2026 10:52:40] [pool www] pid 41822
script_filename = /var/www/your_domain/index.php
[0x00007f3a1c214a10] mysqli_query() /var/www/your_domain/wp-includes/class-wpdb.php:2351
[0x00007f3a1c214980] _do_query() /var/www/your_domain/wp-includes/class-wpdb.php:2265
[0x00007f3a1c2148e0] query() /var/www/your_domain/wp-includes/class-wpdb.php:3161
Una traza que termina en mysqli_query() o PDO->execute() apunta a la base de datos; una que termina en curl_exec() o file_get_contents() apunta a una llamada a un servicio externo que tarda en responder.
Confirmar que OPcache está activo
OPcache guarda el código PHP ya compilado en memoria y viene activado por defecto en el paquete de Ubuntu. Comprueba que el módulo está cargado en PHP-FPM:
ls /etc/php/8.3/fpm/conf.d/ | grep opcache
10-opcache.ini
Si no aparece, instala y activa el módulo con sudo apt install php8.3-opcache y reinicia php8.3-fpm.
Paso 5: Encontrar consultas lentas en MySQL
Activar el registro de consultas lentas
MySQL puede registrar cada consulta que supere un umbral. Actívalo en caliente, sin reiniciar el servicio:
sudo mysql -e "SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log'; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log = 'ON';"
Para que el ajuste sobreviva a un reinicio, añádelo al archivo de configuración del servidor, en la sección [mysqld]:
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
Tras un periodo de tráfico, resume las consultas que más tiempo total consumen con mysqldumpslow, que agrupa las consultas iguales con distintos valores:
sudo mysqldumpslow -s t -t 5 /var/log/mysql/mysql-slow.log
Count: 214 Time=2.41s (515s) Lock=0.00s (0s) Rows=12.0 (2568), wp_user[wp_user]@localhost
SELECT * FROM wp_postmeta WHERE meta_key = 'S' AND meta_value = 'S'
Ver qué se está ejecutando ahora
Si la web está lenta en este momento, consulta las consultas en curso:
sudo mysql -e "SHOW FULL PROCESSLIST;"
Las filas con un valor alto en Time y un estado como Sending data o Waiting for table metadata lock son las que retienen al resto.
Analizar una consulta con EXPLAIN
Toma una consulta del registro y antepón EXPLAIN para ver cómo la ejecuta MySQL:
sudo mysql your_database -e "EXPLAIN SELECT * FROM wp_postmeta WHERE meta_key = 'color' AND meta_value = 'rojo'\G"
type: ALL
possible_keys: meta_key
key: NULL
rows: 1843210
Extra: Using where
type: ALL con key: NULL significa que MySQL recorre la tabla entera. Un índice adecuado suele reducir la consulta de segundos a milisegundos. Crea el índice fuera de las horas de más tráfico y con copia de seguridad reciente, porque en tablas grandes puede tardar:
sudo mysql your_database -e "ALTER TABLE wp_postmeta ADD INDEX idx_meta_key_value (meta_key, meta_value(32));"
Revisar la caché de InnoDB
El buffer pool de InnoDB guarda en memoria los datos e índices más usados. Su tamaño por defecto es de 128 MB, insuficiente si la base de datos ocupa varios GB. Comprueba el valor actual y el tamaño de tus datos:
sudo mysql -e "SELECT @@innodb_buffer_pool_size/1024/1024 AS buffer_pool_mb;"
sudo mysql -e "SELECT table_schema, ROUND(SUM(data_length + index_length)/1024/1024) AS mb FROM information_schema.tables GROUP BY table_schema;"
En un servidor que solo aloja esta web, asigna al buffer pool lo suficiente para que quepan los datos activos, sin superar el 50-60 % de la RAM cuando comparte máquina con Nginx y PHP-FPM. Añade el valor en la sección [mysqld] de mysqld.cnf:
innodb_buffer_pool_size = 1G
Reinicia MySQL para aplicarlo:
sudo systemctl restart mysql
Paso 6: Revisar compresión y caché del navegador
Las respuestas de texto sin comprimir y los archivos estáticos que el navegador vuelve a descargar en cada visita hacen que la web parezca lenta aunque el servidor responda rápido. Comprueba las cabeceras de una hoja de estilos del sitio (sustituye la ruta por una real, la ves en el código fuente de la página):
curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip' https://your_domain/css/style.css | grep -i -E 'content-encoding|cache-control'
Si no aparece content-encoding: gzip, Nginx solo está comprimiendo HTML, que es lo único que comprime con la configuración por defecto. El nginx.conf de Ubuntu ya incluye gzip on;, así que añade el resto de tipos en un archivo propio:
sudo nano /etc/nginx/conf.d/gzip.conf
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json image/svg+xml text/plain text/xml application/xml;
Si has descomentado alguna de estas directivas en /etc/nginx/nginx.conf, no la repitas aquí: Nginx rechaza las directivas duplicadas. Para que el navegador reutilice los estáticos, añade este bloque dentro del server de tu sitio:
location ~* \.(css|js|png|jpg|jpeg|gif|webp|svg|ico|woff2)$ {
expires 30d;
access_log off;
}
Comprueba y recarga:
sudo nginx -t
sudo systemctl reload nginx
Vuelve a ejecutar el curl del principio de este paso: ahora debe mostrar content-encoding: gzip y cache-control: max-age=2592000.
Paso 7: Validar con una prueba de carga
Una prueba de carga confirma si los cambios han mejorado la capacidad del servidor. Instala Apache Bench en la máquina desde la que vas a medir (no en el propio servidor, para no competir por la CPU):
sudo apt install apache2-utils
Lanza 500 peticiones con 20 conexiones simultáneas contra la página que antes era lenta:
ab -n 500 -c 20 https://your_domain/
Concurrency Level: 20
Time taken for tests: 9.842 seconds
Complete requests: 500
Failed requests: 0
Requests per second: 50.80 [#/sec] (mean)
Time per request: 393.701 [ms] (mean)
Percentage of the requests served within a certain time (ms)
50% 371
95% 512
99% 688
100% 802 (longest request)
Anota Requests per second y los percentiles 95 y 99 antes y después de cada cambio. Mide los cambios de uno en uno para saber cuál ha tenido efecto.
Advertenciauna prueba de carga contra producción afecta a los visitantes reales. Hazla en horas de poco tráfico y empieza con una concurrencia baja.
Solución de problemas
Nginx devuelve 504 Gateway Time-out: PHP-FPM no respondió a tiempo. Revisa el slowlog del paso 4 y el registro de consultas lentas del paso 5 para encontrar la petición que tarda. Aumentar fastcgi_read_timeout solo oculta el problema.
Nginx devuelve 502 Bad Gateway bajo carga: todos los procesos de PHP-FPM están ocupados o se han cerrado por falta de memoria. Busca max_children en /var/log/php8.3-fpm.log y Out of memory en sudo journalctl -k.
El servidor usa swap aunque hay poco tráfico: la suma de innodb_buffer_pool_size y pm.max_children multiplicado por la memoria de cada proceso PHP supera la RAM. Reduce uno de los dos o amplía el plan.
La web va lenta solo en algunas horas: compara las horas lentas con el registro de Nginx del paso 3. Una tarea programada (copias de seguridad, wp-cron) o un rastreador que hace miles de peticiones suele coincidir con esas horas.
Conclusión
Has medido una petición por fases, comprobado los recursos del servidor, registrado el tiempo de cada petición en Nginx y localizado las partes lentas en PHP-FPM y MySQL, además de validar los cambios con una prueba de carga. Como siguientes pasos, añade una caché de página (FastCGI cache de Nginx o un plugin de caché si usas WordPress), usa Redis como caché de objetos para reducir las consultas a MySQL y configura una monitorización continua que avise cuando el tiempo de respuesta suba.
