TTFB alto en WordPress: acelera la respuesta del servidor sin plugins mágicos

¿Te fue útil?

Un TTFB alto es el tipo de problema de rendimiento que hace que todo el mundo esté confiado y equivocado al mismo tiempo. Marketing dice “el sitio está lento”, un proveedor de plugins dice “instala esto” y tu proveedor de hosting dice “es WordPress”. Mientras tanto, tus usuarios miran una página en blanco esperando el primer byte como si fuera 2006 y estuviéramos todos en dial‑up otra vez.

Esto no es una comparativa de plugins. Es un playbook de producción para controlar el tiempo de respuesta del servidor: mide dónde se atasca el primer byte (red, TLS, servidor web, PHP, base de datos, caché, almacenamiento), arregla el cuello de botella y evita cambios que se ven rápidos en una gráfica pero se derriten bajo tráfico real.

TTFB: realidad y malentendidos

Time To First Byte es el tiempo desde que el cliente inicia la petición hasta que recibe el primer byte del cuerpo de la respuesta. Eso incluye:

  • Resolución DNS (a veces, según la herramienta)
  • Conexión TCP
  • Handshake TLS
  • Enrutamiento de la petición (load balancer, reverse proxy)
  • Procesamiento del servidor web y esperas de upstream
  • Trabajo de la aplicación (PHP + WordPress)
  • Consultas a la base de datos
  • Fallos de caché, stampedes de caché y momentos de “¿por qué Redis está al 100% de CPU?”
  • Latencia de almacenamiento (especialmente en volúmenes compartidos o con bursting)

TTFB no es un simple “tiempo de backend”. Es una métrica de extremo a extremo vista por el cliente. Por eso es útil y fácil de usar mal. Si mides TTFB desde tu portátil en el Wi‑Fi del hotel, estás aprendiendo sobre el hotel. Si lo mides desde dentro del mismo datacenter, estás aprendiendo sobre tu stack.

Además: un TTFB bajo no garantiza una página rápida. Solo garantiza que el servidor empezó a transmitir rápido. Puedes seguir enviando 7 MB de JavaScript e incendiar el navegador. Pero cuando el TTFB es alto, rara vez es un problema del front‑end. Es el sistema que no llega al “primer byte” a tiempo—por lo general esperando algo.

Regla opinada: no trates un TTFB alto como “WordPress es lento”. Trátalo como “una petición está bloqueada”. Luego encuentra en qué se bloquea.

Hechos interesantes y un poco de historia

  1. TTFB existe desde antes de Core Web Vitals. Los ingenieros lo han usado durante décadas porque corresponde bien a la latencia del lado servidor y al coste del handshake de red.
  2. WordPress comenzó en 2003 como un fork de b2/cafelog. El hosting compartido temprano moldeó muchos valores por defecto: PHP, MySQL y supuestos de caché “mantenerlo simple”.
  3. El opcode caching en PHP solía ser opcional y caótico. Antes de que OPcache fuera estándar, ejecutar WordPress significaba parsear PHP en cada petición—el dolor de TTFB era normal.
  4. keep‑alive de HTTP/1.1 (finales de los 90) fue una revolución silenciosa. Sin él, las conexiones repetidas inflaban masivamente la latencia percibida.
  5. Los handshakes TLS solían ser caros. TLS 1.3 moderno redujo rondas y la reanudación de sesión es importante para visitantes repetidos.
  6. El query cache de MySQL fue eliminado porque causaba contención de locks y rendimiento impredecible en cargas con muchas escrituras—común en WP admin y WooCommerce.
  7. “La caché de página lo arregla todo” fue cierto en cierta medida cuando los sitios eran mayormente tráfico anónimo. Usuarios autenticados, personalización y carros cambiaron la ecuación.
  8. El object caching para WordPress maduró tarde. Cachés persistentes (Redis/Memcached) se hicieron comunes a medida que los sitios WP se volvieron más dinámicos y con muchos plugins.
  9. Los CDN no eliminaron los problemas de TTFB. Pueden ocultarlos para páginas cacheadas, pero las peticiones dinámicas y misses de caché aún vuelven al origin como si le debieran dinero.

Playbook de diagnóstico rápido

Cuando el TTFB sube, no empieces instalando plugins, cambiando temas o discutiendo en Slack. Comienza respondiendo tres preguntas rápidamente:

1) ¿Es red/TLS o servidor/aplicación?

  • Mide el TTFB desde afuera y desde dentro del servidor/VPC.
  • Si el TTFB interno está bien pero el externo es malo: piensa en DNS, handshake TLS, load balancer, enrutamiento, pérdida de paquetes o WAF.
  • Si el interno también es malo: es tu stack de app (PHP, DB, caché, almacenamiento, contención de CPU).

2) ¿Es un endpoint o es todo?

  • ¿Solo la página principal? Podría ser una consulta lenta o un tema/plantilla haciendo demasiado.
  • ¿Solo wp-admin? Podría ser autenticación, llamadas externas o contención de locks en la BD.
  • ¿Todo (incluyendo assets estáticos)? Piensa en saturación del servidor web, CPU steal o límites de red upstream.

3) ¿Es “lento todo el tiempo” o “lento bajo concurrencia”?

  • Lento incluso a 1 petición: busca cuellos de botella por petición única (búsquedas DNS desde PHP, consulta DB lenta, latencia de almacenamiento, arranque de PHP/OPcache mal configurado).
  • Rápido a 1 petición pero lento a 20: busca dimensionado de pools, conexiones DB, locks, stampedes de caché, limitación de tasa, saturación de CPU.

Condición de parada: Cuando puedas decir “estamos esperando X durante Y milisegundos”, has terminado el diagnóstico y puedes empezar a arreglar. Si no puedes decir eso, sigues adivinando.

Mide primero: construye la línea de tiempo de una petición

El TTFB es un solo número. Necesitas una línea de tiempo. El objetivo es dividir la petición en fases y asignar números a cada fase:

  • Búsqueda de nombre (DNS)
  • Conexión (TCP)
  • Conexión de la app (handshake TLS)
  • Pre‑transfer (petición enviada, esperando respuesta)
  • Inicio de transferencia (el propio TTFB)
  • Total (respuesta completa)

Desde el lado del servidor quieres:

  • Tiempos de Nginx/Apache: request time, upstream response time
  • Tiempos de PHP-FPM: duración de la petición, stack traces del slowlog
  • Tiempos DB: slow query log, esperas de locks, tasa de aciertos del buffer pool
  • Tiempos del sistema: presión de CPU, run queue, latencia de I/O, retransmisiones de red

Broma #1: Si tu monitorización es “refrescar la página y entrecerrar los ojos”, no tienes monitorización; tienes un ritual.

Una cita, porque es eterna: La esperanza no es una estrategia. — James Cameron.

Tareas prácticas con comandos: qué ejecutar, qué significa, qué decides

Estos no son “prueba esto tal vez”. Son las jugadas que puedes hacer en un host Linux con WordPress y Nginx + PHP-FPM + MariaDB/MySQL (ideas similares aplican a Apache). Cada tarea incluye un comando, ejemplo de salida, qué significa la salida y la decisión que tomas.

Task 1: Measure TTFB from a client with curl (baseline)

cr0x@server:~$ curl -o /dev/null -s -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://example.com/
dns=0.012 connect=0.031 tls=0.082 ttfb=1.247 total=1.392

Significado: DNS/conexión/TLS son pequeñas; el TTFB es grande. El servidor/aplicación tarda en producir el primer byte.

Decisión: Enfócate en el procesamiento en origen (esperas upstream de Nginx, PHP-FPM, BD, cachés), no en red o TLS.

Task 2: Measure TTFB from inside the server/VPC (separate network from app)

cr0x@server:~$ curl -o /dev/null -s -w 'connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://127.0.0.1/
connect=0.000 tls=0.000 ttfb=1.106 total=1.219

Significado: Todavía lento internamente. Esto no es tu ISP, ni DNS, ni CDN.

Decisión: Instrumenta el servidor: logs del servidor web, estado/slowlog de PHP-FPM, consultas lentas en BD, y checks de recursos del sistema.

Task 3: Split static vs dynamic quickly

cr0x@server:~$ curl -o /dev/null -s -w 'ttfb=%{time_starttransfer} total=%{time_total}\n' https://example.com/wp-content/uploads/2025/01/logo.png
ttfb=0.045 total=0.046

Significado: Lo estático está bien; lo dinámico es el problema.

Decisión: No pierdas tiempo afinando sendfile o gzip ahora. Ve directo a PHP/BD/caché.

Task 4: Turn on request timing in Nginx access logs (see upstream time)

cr0x@server:~$ sudo grep -R "log_format" /etc/nginx/nginx.conf /etc/nginx/conf.d 2>/dev/null
/etc/nginx/nginx.conf:log_format main '$remote_addr - $host [$time_local] "$request" $status $body_bytes_sent '
/etc/nginx/nginx.conf:'"$http_referer" "$http_user_agent" rt=$request_time urt=$upstream_response_time uct=$upstream_connect_time';

Significado: Ya tienes variables para request_time y tiempos upstream. Bien.

Decisión: Asegura que el server block activo use ese log_format y luego inspecciona solicitudes lentas.

Task 5: Find the slowest requests in Nginx logs (real URLs, real timings)

cr0x@server:~$ sudo awk '{for(i=1;i<=NF;i++) if($i ~ /^rt=/){sub("rt=","",$i); print $i, $0}}' /var/log/nginx/access.log | sort -nr | head -n 5
2.914 203.0.113.10 - example.com [27/Dec/2025:10:11:09 +0000] "GET /product/widget/ HTTP/2.0" 200 84512 "-" "Mozilla/5.0" rt=2.914 urt=2.902 uct=0.000
2.501 203.0.113.11 - example.com [27/Dec/2025:10:10:58 +0000] "GET / HTTP/2.0" 200 112340 "-" "Mozilla/5.0" rt=2.501 urt=2.489 uct=0.000

Significado: request_time (rt) coincide aproximadamente con upstream_response_time (urt). Nginx no es el cuello de botella; está esperando al upstream (PHP-FPM).

Decisión: Pasa a PHP-FPM: saturación del pool, scripts lentos, llamadas externas, OPcache.

Task 6: Check PHP-FPM pool health and saturation

cr0x@server:~$ curl -s http://127.0.0.1/fpm-status?full | sed -n '1,25p'
pool:                 www
process manager:      dynamic
start time:           27/Dec/2025:09:31:02 +0000
start since:          2430
accepted conn:        18291
listen queue:         17
max listen queue:     94
listen queue len:     128
idle processes:       0
active processes:     24
total processes:      24
max active processes: 24
max children reached: 63
slow requests:        211

Significado: Existe listen queue y max children reached es distinto de cero. PHP-FPM está saturado: las peticiones esperan antes de que siquiera empiece a correr PHP.

Decisión: Ajusta PHP-FPM: incrementa pm.max_children si CPU/RAM lo permiten, y reduce el coste por petición (OPcache, BD, cachés). También confirma que no estás ocultando una BD lenta detrás de “más children”.

Task 7: Check PHP-FPM slowlog for where time is going

cr0x@server:~$ sudo tail -n 30 /var/log/php8.2-fpm/www-slow.log
[27-Dec-2025 10:11:09]  [pool www] pid 22107
script_filename = /var/www/html/index.php
[0x00007f2a2c1a3f40] curl_exec() /var/www/html/wp-includes/Requests/src/Transport/Curl.php:205
[0x00007f2a2c19b8a0] request() /var/www/html/wp-includes/Requests/src/Session.php:214
[0x00007f2a2c193250] wp_remote_get() /var/www/html/wp-includes/http.php:197
[0x00007f2a2c18aa10] some_plugin_check_updates() /var/www/html/wp-content/plugins/some-plugin/plugin.php:812

Significado: Un plugin está haciendo una llamada HTTP saliente durante la generación de la página. Eso arruina el TTFB en cuanto el servicio remoto se pone lento o bloqueado.

Decisión: Elimina/reemplaza el comportamiento del plugin, muévelo a cron/threads en background o cachea el resultado agresivamente. También verifica DNS de salida y reglas de firewall de egress.

Task 8: Verify OPcache is enabled and not starved

cr0x@server:~$ php -i | grep -E 'opcache.enable|opcache.memory_consumption|opcache.interned_strings_buffer|opcache.max_accelerated_files|opcache.validate_timestamps'
opcache.enable => On => On
opcache.memory_consumption => 128 => 128
opcache.interned_strings_buffer => 8 => 8
opcache.max_accelerated_files => 10000 => 10000
opcache.validate_timestamps => On => On

Significado: OPcache está activado, pero 128MB y 10k archivos pueden ser ajustados para sitios con muchos plugins. La validación de timestamps en producción también añade comprobaciones de sistema de ficheros.

Decisión: Aumenta memoria de OPcache y max_accelerated_files; considera poner opcache.validate_timestamps=0 en despliegues inmutables (con reinicio en deploy).

Task 9: Check database slow queries (you can’t cache your way out of locks)

cr0x@server:~$ sudo tail -n 20 /var/log/mysql/mysql-slow.log
# Time: 2025-12-27T10:10:58.114221Z
# Query_time: 1.873  Lock_time: 0.001 Rows_sent: 20  Rows_examined: 945321
SET timestamp=1766830258;
SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes';

Significado: Esa consulta de options autoload está escaneando demasiadas filas. Normalmente es autoload bloat (los plugins meten cosas), o índices faltantes/combinados con bloat de tablas.

Decisión: Reduce el bloat de autoload, revisa el tamaño de wp_options y confirma índices apropiados. Considera cache de objetos para evitar hits repetidos a options, pero arregla el bloat de base también.

Task 10: Check InnoDB buffer pool health (are you doing disk I/O for reads?)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+----------+
| Variable_name                         | Value    |
+---------------------------------------+----------+
| Innodb_buffer_pool_read_requests      | 98765432 |
| Innodb_buffer_pool_reads              | 1234567  |
+---------------------------------------+----------+

Significado: Muchas lecturas vienen del disco (Innodb_buffer_pool_reads no es pequeño). Eso añade latencia y aumenta el TTFB bajo carga.

Decisión: Incrementa innodb_buffer_pool_size (dentro de límites de RAM), reduce el tamaño del dataset (limpieza) y verifica la latencia del almacenamiento.

Task 11: Spot DB lock waits in real time (the “everything is stuck” mode)

cr0x@server:~$ mysql -e "SHOW PROCESSLIST;" | head -n 12
Id	User	Host	db	Command	Time	State	Info
291	wpuser	localhost	wp	Query	12	Sending data	SELECT * FROM wp_posts WHERE post_status='publish' ORDER BY post_date DESC LIMIT 10
305	wpuser	localhost	wp	Query	9	Locked	UPDATE wp_options SET option_value='...' WHERE option_name='woocommerce_sessions'

Significado: Las consultas esperan locks. Esto suele parecer “picos aleatorios de TTFB” porque algunas peticiones quedan detrás de una escritura o de una consulta de larga duración.

Decisión: Identifica la consulta que bloquea, arregla el patrón (índices, reducir escrituras de sesiones, evitar tormentas de autocommit) y considera aislar trabajos administrativos pesados del tráfico front‑end.

Task 12: Check CPU pressure and run queue (are requests waiting for CPU?)

cr0x@server:~$ uptime
 10:12:33 up 12 days,  3:41,  2 users,  load average: 18.42, 17.90, 16.85

Significado: El load average es muy alto. Si tienes, digamos, 8 vCPU, estás por encima de capacidad y los workers PHP se encolarán.

Decisión: Reduce el coste de CPU por petición (caché, OPcache, menos plugins), limita la concurrencia o añade CPU. No aumentes pm.max_children a ciegas en una máquina saturada de CPU.

Task 13: Find I/O latency spikes (storage can dominate TTFB)

cr0x@server:~$ iostat -xz 1 3
Linux 6.1.0 (server) 	12/27/2025 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          22.10    0.00    6.30    9.80    0.00   61.80

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s  w_await aqu-sz  %util
nvme0n1         210.0  18560.0     0.0   0.00   18.40    88.38    95.0   9120.0   42.10   5.12  98.20

Significado: Await alto y util casi 100%: el almacenamiento está saturado. Lecturas/escrituras de BD y llamadas al sistema de ficheros se bloquearán.

Decisión: Pasa a almacenamiento más rápido, reduce I/O (buffer pool, cachés), investiga noisy neighbors (discos compartidos) y verifica jobs de backup o tormentas de logs.

Task 14: Check network retransmits and drops (quiet latency killers)

cr0x@server:~$ netstat -s | egrep -i 'retrans|segments retransmited|listen|overflow' | head -n 12
    14572 segments retransmited
    0 listen queue overflows
    0 listen queue drops

Significado: Hay retransmisiones. Si suben rápidamente, tienes pérdida de paquetes o congestión.

Decisión: Revisa errores de interfaz, la red upstream y la salud del load balancer. Retransmisiones altas pueden inflar el TTFB incluso cuando la app está bien.

Task 15: Verify Nginx isn’t buffering you into latency (rare, but real)

cr0x@server:~$ sudo nginx -T 2>/dev/null | egrep -n 'proxy_buffering|fastcgi_buffering|fastcgi_read_timeout|keepalive_timeout' | head -n 20
42:	keepalive_timeout 65;
117:	fastcgi_read_timeout 60s;
118:	fastcgi_buffering on;

Significado: El buffering está activado. Normalmente ayuda al throughput, pero puede ocultar respuestas en streaming y hacer el debugging más difícil. Los timeouts son normales.

Decisión: No apagues el buffering por superstición. Úsalo intencionalmente (en general déjalo activado). Si tienes peticiones de larga duración, arregla la causa, no el timeout.

Task 16: Confirm HTTP keep-alive and connection reuse (reduce handshake cost)

cr0x@server:~$ curl -I -s https://example.com/ | egrep -i 'server:|connection:|keep-alive|alt-svc|via'
server: nginx
connection: keep-alive

Significado: Keep‑alive está habilitado. Bien. Si vieras connection: close en todas partes, pagarías overhead de handshake extra repetidamente.

Decisión: Mantenlo. Ajusta timeouts sensatamente. No “optimices” cerrando conexiones temprano a menos que te guste la latencia autoinfligida.

Task 17: Look for external calls from WordPress at the network layer

cr0x@server:~$ sudo ss -tpn | grep php-fpm | head -n 10
ESTAB 0 0 10.0.0.10:48722 93.184.216.34:443 users:(("php-fpm8.2",pid=22107,fd=15))
ESTAB 0 0 10.0.0.10:48724 151.101.2.132:443 users:(("php-fpm8.2",pid=22111,fd=17))

Significado: Workers de PHP-FPM están haciendo conexiones HTTPS salientes mientras sirven peticiones. Eso es una bomba de tiempo para el TTFB.

Decisión: Identifica qué plugin/tema lo hace; muévelo a jobs en background; cachea resultados; aplica timeouts; bloquea egress inesperado si puedes.

Task 18: Quick “what changed?” sanity check (package updates, deploys, restarts)

cr0x@server:~$ sudo journalctl -p warning -S "2 hours ago" | head -n 12
Dec 27 09:02:11 server php-fpm8.2[1022]: WARNING: [pool www] server reached pm.max_children setting (24), consider raising it
Dec 27 09:05:44 server mariadbd[881]: Aborted connection 305 to db: 'wp' user: 'wpuser' host: 'localhost' (Got timeout reading communication packets)

Significado: Tienes corroboración: saturación de PHP-FPM y timeouts de comunicación con BD (posiblemente por sobrecarga).

Decisión: Trátalo como capacidad/latencia, no como “misteriosa lentitud de WordPress”. Arregla el dimensionado del pool y el rendimiento de la BD; considera escalar.

Arreglos que realmente reducen el TTFB

1) Deja de hacer trabajo en cada petición: caché de página donde tenga sentido

Para tráfico anónimo, el cacheo de página completo es la palanca más grande. No porque sea tendencia, sino porque elimina PHP y BD de la ruta de la petición. Puedes hacerlo en:

  • Nginx fastcgi_cache
  • Capa de reverse proxy (Varnish o un edge cache gestionado)
  • CDN que cachee HTML (cuidado con cookies y lógica Vary)

Pero tienes que ser honesto sobre tu sitio:

  • Si usas mucho WooCommerce, el tráfico más valioso suele ser de usuarios logueados o con cookies. La tasa de aciertos de cache puede ser baja.
  • Si tienes mucha personalización, el caching se convierte en un problema de enrutamiento: ¿qué varía y por qué?

Consejo para decidir: si la homepage es cacheable pero las páginas de producto no, aún así el cache gana. Arregla lo que puedas cachear primero.

2) Caché de objetos persistente: Redis (o Memcached) para las partes que WordPress repite

WordPress hace las mismas búsquedas repetidamente: options, post meta, relaciones de términos, transients. Una caché de objetos persistente reduce viajes a la BD y ayuda al TTFB para páginas dinámicas.

Qué vigilar:

  • La caché de objetos no arregla llamadas HTTP externas lentas.
  • La caché de objetos no arregla contención de locks en BD causada por patrones de escritura.
  • La caché de objetos puede empeorar si la conviertes en un basurero compartido sin higiene de keys y sin estrategia de expulsión.

3) PHP-FPM: ajusta para tu hardware, no para tus deseos

Los ajustes de PHP-FPM son fáciles de cambiar y más fáciles de estropear. Tu objetivo es evitar colas mientras mantienes CPU y RAM estables.

  • Síntoma: listen queue y max children reached suben durante picos.
  • Arreglo: Aumenta pm.max_children si tienes margen, y reduce el tiempo por petición para que los children se liberen más rápido.
  • Anti‑arreglo: Duplicar children en una máquina con poca RAM. Entrarás en swapping, y swapping es solo latencia con pasos extra.

Calcula un techo aproximado: mide la memoria por proceso PHP-FPM bajo carga, luego fija children para que el total no exceda la RAM disponible menos DB y caché del SO.

4) OPcache: dale a PHP un cerebro y suficiente espacio para usarlo

Sin OPcache, PHP reparsea scripts y gasta CPU y llamadas al sistema de ficheros. Con OPcache pero poca memoria, hace thrashing y vuelves a arrancadas lentas.

Haz esto:

  • Aumenta opcache.memory_consumption para sitios con muchos plugins.
  • Aumenta opcache.max_accelerated_files para cubrir theme + plugins.
  • Prefiere patrones de despliegue inmutables; desactiva la validación de timestamps cuando sea seguro.

5) Base de datos: haz que las consultas calientes sean baratas y que las esperas de locks sean raras

La mayoría del dolor de BD en WordPress no es exótico. Son básicas que ya sabes que deberías hacer:

  • Mantén bajo el autoload de wp_options. Los plugins lo inflan; pagas esa tasa en cada petición.
  • Indexa lo que consultas. Muchos plugins llevan consultas cuestionables y asumen que la BD “lo resolverá”. La BD lo resolverá: será lenta.
  • Da a InnoDB suficiente buffer pool. Si las lecturas van al disco, el TTFB se convierte en un benchmark de almacenamiento.
  • Vigila tablas con mucha escritura de locks (sessions, options, postmeta) durante cargas de escritura.

6) Almacenamiento: elimina la latencia cola, no solo la media

El TTFB sufre por la latencia cola. El percentil 99 importa porque es lo que recuerdan los usuarios y lo que alerta tu monitorización.

Asesinos comunes de TTFB por almacenamiento:

  • Volúmenes de red compartidos con créditos de burst (rápidos hasta que no lo son)
  • Backups ocupando el mismo disco que MySQL
  • Tormentas de logs (debug, access, slow logs en un único volumen)
  • Agitación del metadata del filesystem (toneladas de llamadas stat() si OPcache valida timestamps y no hay realpath cache tuning)

7) Llamadas externas: hazlas asíncronas o haz que desaparezcan

Si WordPress espera APIs de terceros durante el procesamiento de la petición, convertiste tu SLA en el hobby de ellos. Aplica timeouts estrictos a llamadas salientes, cachea resultados y mueve trabajo no crítico a cron/colas.

Broma #2: Las APIs de terceros son como ascensores: el momento en que llegas tarde, todos están ocupados.

8) CDN y edge: úsalo para reducir hits al origin, no para ocultar incendios en origen

Un CDN ayuda cuando puedes cachear HTML y activos estáticos. También reduce costes de TLS y TCP para usuarios globales. Pero si tu origin tiene un TTFB de 1.5s en páginas dinámicas no cacheadas, el CDN es solo un mensajero educado entregando malas noticias más rápido.

Tres micro‑historias del mundo corporativo (y las lecciones)

Historia 1: El incidente causado por una suposición equivocada

El equipo tenía un sitio WordPress que “iba bien” en la oficina y “era horrible” para usuarios en el extranjero. Revisaron algunos informes de Lighthouse y culparon al theme. Tras un sprint el theme era más ligero y el problema… seguía igual.

En una revisión del incidente en producción, alguien finalmente midió el TTFB desde dos lugares: desde el servidor y desde una región remota. Dentro de la VPC, el TTFB era consistentemente bajo. Desde fuera, subía y oscilaba. La aplicación no estaba lenta; el camino hacia la aplicación lo estaba.

El load balancer terminaba TLS y un conjunto de reglas WAF hacía inspección profunda en cada petición—incluidas las cacheadas. Bajo picos, la cola de inspección creció y el TTFB con ella. Todos asumieron “WAF es básicamente gratis”. No lo era.

Lo arreglaron afinando el conjunto de reglas, evitando inspección para rutas estáticas conocidas y añadiendo capacidad donde importaba. Los cambios de theme ayudaron un poco, pero la solución real fue admitir la suposición equivocada: “TTFB = tiempo PHP”. No era.

Historia 2: La optimización que salió mal

Otra empresa decidió ganar la batalla del TTFB aumentando agresivamente pm.max_children de PHP-FPM. La gráfica se veía bien en staging. En producción, fue un éxito espectacular por unos diez minutos.

Luego la base de datos empezó a timeoutear. No porque estuviera infra‑provisionada, sino porque la nueva concurrencia PHP la estampó. El número de consultas simultáneas se disparó, el buffer pool churn aumentó y la contención de locks apareció en lugares que nadie había perfilado. El TTFB pasó de “meh” a “derrumbe”.

Para hacerlo más picante, el uso de memoria saltó. La máquina empezó a hacer swapping. Cuando el swap entra en escena, la latencia se vuelve una danza interpretativa: a veces bien, a veces horrible, siempre difícil de razonar.

La solución eventual fue aburrida: limitar la concurrencia PHP a lo que la BD podía soportar, habilitar caché persistente de objetos, limpiar algunas consultas patológicas y dimensionar correctamente el buffer pool. También añadieron una prueba de carga que medía encolamiento, no solo latencia media. La lección: si “optimizar” es aumentar paralelismo, no estás optimizando—estás negociando con tu cuello de botella.

Historia 3: La práctica aburrida pero correcta que salvó el día

Un sitio de medios corría WordPress detrás de Nginx y una capa de caché. Tenían un hábito que me encanta: cuando el rendimiento se veía raro, tiraban de tres logs antes de tocar nada—access log de Nginx con tiempo upstream, slowlog de PHP-FPM y slow query log de MySQL.

Una tarde, el TTFB subió de forma intermitente. El on‑call podría haber hecho la danza de siempre: reiniciar PHP, limpiar cachés, culpar el último deploy. En lugar de eso, siguieron el hábito. Nginx mostró spikes en upstream response time. El slowlog de PHP mostró workers atascados en llamadas al sistema de ficheros. El slow log de MySQL estaba tranquilo.

Revisaron métricas de almacenamiento y encontraron picos de latencia alineados con un backup automatizado que se había desplazado a horario laboral por un cambio de zona horaria. Nadie tuvo que adivinar. Nadie tuvo que pelear. Movieron la ventana de backup y añadieron una alerta por await de disco sostenido.

Esto no fue ingeniería heroica. Fue higiene operativa simple. La que luce poco hasta que te salva el fin de semana.

Errores comunes: síntoma → causa raíz → solución

  • Síntoma: TTFB alto solo en la primera petición tras deploy/restart
    Causa raíz: OPcache frío, caché de página/objetos fría, warmup de JIT, caché DNS vacío
    Solución: Precalienta cachés (preload de URLs), asegura OPcache con tamaño correcto, evita reinicios frecuentes y pre‑resuelve DNS salientes críticos si debes llamar a terceros.
  • Síntoma: Picos de TTFB durante picos de tráfico, bien fuera de hora
    Causa raíz: Saturación y encolamiento de PHP-FPM, límites de conexiones DB, stampede de caché
    Solución: Dimensiona correctamente pm.max_children, añade caché de página/objetos, implementa locking de caché o coalescencia de solicitudes, y reduce endpoints caros sin caché.
  • Síntoma: wp-admin se siente lento, front‑end en su mayoría bien
    Causa raíz: Las peticiones admin evitan la caché de página; consultas pesadas, comprobaciones de actualización de plugins, llamadas remotas
    Solución: Perfila endpoints de admin; desactiva o programa comprobaciones de actualización; arregla consultas lentas; separa tráfico admin si es necesario.
  • Síntoma: Assets estáticos rápidos, HTML lento
    Causa raíz: Trabajo de PHP/BD domina; sin caché efectivo; llamadas externas lentas
    Solución: Añade caché de página donde sea seguro; añade caché de objetos persistente; elimina llamadas remotas síncronas; afina OPcache y BD.
  • Síntoma: Outliers aleatorios de TTFB (p95/p99 feo)
    Causa raíz: Latencia cola de almacenamiento, esperas de locks BD, efectos de noisy neighbor, cron jobs solapándose con tráfico
    Solución: Mueve la BD a almacenamiento estable y baja latencia; reduce contención de locks; reprograma cron/backups; añade alertas en latencia de I/O y esperas de locks.
  • Síntoma: Aumentar children de PHP-FPM lo empeoró
    Causa raíz: La BD se convirtió en cuello de botella; presión de memoria provocó swapping
    Solución: Reduce la concurrencia; mide RSS por proceso; afina BD y cachés primero; escala vertical/horizontal con intención.
  • Síntoma: Monitorización externa muestra TTFB alto, chequeos internos bien
    Causa raíz: DNS, handshake TLS, colas WAF/LB, pérdida de paquetes
    Solución: Mide con curl timings desde múltiples regiones; inspecciona métricas de LB/WAF; ajusta reanudación TLS; arregla pérdida de red; evita inspección costosa para rutas seguras.
  • Síntoma: TTFB empeoró tras habilitar un plugin de caché
    Causa raíz: Config de caché provoca contención, tormentas de purga, o desactiva patrones amigables con OPcache
    Solución: Prefiere cachear en Nginx/proxy donde sea posible; asegura que las claves de caché sean correctas; evita purgar todo el cache por cambios menores; valida con pruebas de carga.

Listas de verificación / plan paso a paso

Paso a paso: de “TTFB está mal” a una solución específica en una sesión de trabajo

  1. Establece líneas base: ejecuta curl timing desde tu portátil y desde el servidor. Guarda los números.
  2. Confirma el alcance: prueba la homepage, una entrada típica, una página de producto y un asset estático. Identifica lentitud “solo dinámica”.
  3. Lee la verdad del servidor web: habilita/verifica campos rt y urt en Nginx. Encuentra URLs más lentas.
  4. Revisa saturación de PHP-FPM: mira listen queue y max children reached. Decide si estás encolando.
  5. Activa PHP slowlog (si no está) con un umbral manejable (e.g., 2s). Captura stack traces durante la lentitud.
  6. Revisa llamadas salientes: stacks del slowlog y salida de ss. Si están presentes, arregla eso primero—nada más importa.
  7. Revisa consultas lentas y locks: activa slow query log; inspecciona processlist para locks.
  8. Revisa latencia de almacenamiento: usa iostat. Si await y util están mal, trata el almacenamiento como sospechoso primario.
  9. Implementa un cambio a la vez: capa de caché, ajuste de OPcache, dimensionado buffer pool BD, ajuste de pool PHP.
  10. Valida con las mismas medidas: curl timing y tiempos en logs. Mantén evidencia antes/después.

Checklist operativa: evita que el TTFB recaiga el próximo mes

  • Los access logs incluyen request time y upstream response time.
  • El endpoint de estado de PHP-FPM está protegido y monitorizado (cola, procesos activos, max children reached).
  • Los slow logs están activados (PHP y BD) con retención sensata.
  • Backups y cron pesados están programados y monitorizados para deriva de tiempo de ejecución.
  • El proceso de deploy precalienta cachés o al menos evita arranques fríos simultáneos en todos los nodos.
  • Hay una prueba de carga que mide p95/p99, no solo la media.
  • Existe una política de plugins: cualquier cosa que haga I/O externo síncrono en la ruta de petición se trata como riesgo de producción.

FAQ

1) ¿Cuál es un TTFB “bueno” para WordPress?

Para HTML cacheado en el edge o proxy: decenas de milisegundos hasta cientos bajos. Para WordPress dinámico no cacheado: apuntar a sub‑400ms en p50 es razonable en un stack sano. Tu p95 es lo que te va a dañar públicamente.

2) ¿Por qué el TTFB es alto aunque la CPU parezca baja?

Porque puedes estar bloqueado sin estar limitado por CPU: esperando I/O de disco, locks de BD, llamadas HTTP externas, timeouts DNS, o una cola saturada de PHP-FPM donde los workers están atascados en otra cosa.

3) ¿Necesito un plugin de caché?

No necesariamente. Cachear en el servidor/proxy suele ser más predecible y rápido. Los plugins pueden ayudar a gestionar purgas de caché, pero también añaden complejidad y modos de fallo. Si usas uno, trátalo como infraestructura: configúralo, mídelo y pruébalo.

4) Si añado Redis, ¿el TTFB bajará mágicamente?

No. Redis ayuda cuando la BD hace lecturas repetitivas (options, meta, taxonomía). No arreglará código PHP lento, llamadas remotas o contención de locks por comportamiento de escrituras. Es una palanca, no una religión.

5) ¿Debería simplemente aumentar pm.max_children de PHP-FPM?

Sólo si has probado que estás encolando y tienes margen de CPU/RAM. Si no, aumentarás la concurrencia hasta chocar con el verdadero cuello de botella (a menudo la BD) y empeorarás la latencia.

6) ¿Por qué el TTFB sube aleatoriamente de noche?

Porque “de noche” es cuando salen cron jobs y backups. Busca dumps de BD, snapshots de filesystem, rotación de logs, escaneos anti‑malware o jobs de plugins programados que se han desplazado a ventanas pico.

7) ¿Cómo sé si la BD es el cuello de botella sin APM caro?

Correlaciona: spikes en upstream response time de Nginx + slowlog de PHP mostrando tiempo en llamadas BD + slow log/esperas de locks en MySQL. Si el tiempo de BD domina, lo verás en esas señales.

8) ¿HTTP/2 o HTTP/3 reducen el TTFB?

Pueden reducir overhead de conexión y mejorar multiplexing, especialmente en redes con pérdida. Pero si tu origin tarda 1–2 segundos en construir HTML, el protocolo no te salvará. Arregla primero el origin y después disfruta las ganancias del protocolo.

9) ¿Vale la pena un CDN si mi TTFB es alto?

Sí para assets estáticos y HTML cacheable, porque reduce la carga al origin y mejora la latencia global. Pero no lo uses como excusa para ignorar el rendimiento de origen. Los misses de caché seguirán revelando la verdad.

10) ¿Por qué wp-admin está más lento después de “optimizar” el front‑end?

Porque la mayoría de optimizaciones que hiciste fueron para páginas cacheables. wp-admin es dinámico, suele evitar caches y activa comportamientos extra de plugins (comprobaciones de actualización, analíticas, APIs remotas). Perfílalo por separado.

Siguientes pasos que puedes hacer esta semana

  1. Consigue los números: curl timing desde dentro y fuera, y logs de Nginx con tiempos upstream.
  2. Prueba o descarta encolamiento de PHP-FPM: habilita y revisa /fpm-status; observa max children reached y listen queue durante carga.
  3. Activa PHP slowlog y deja que te diga la verdad incómoda (normalmente llamadas externas o un camino de código caro).
  4. Activa slow query log por un día, luego arregla los principales culpables y el bloat de autoload.
  5. Valida la latencia de almacenamiento con iostat durante el pico. Si estás en volúmenes bursty o compartidos, planea moverlos.
  6. Añade caché deliberadamente: caché de página para tráfico anónimo; caché de objetos para repetición dinámica; mantén claves y purgas de caché sensatas.
  7. Retesta y formaliza: mismas mediciones curl, misma inspección de logs y una prueba de carga pequeña. Anota los resultados para que el tú del futuro no repita la misma investigación.

Si mides y aún no puedes explicar a dónde va el tiempo, esa es la señal para añadir tracing (aunque sea ligero) y dejar de adivinar. Los sistemas en producción no responden a las sensaciones.

← Anterior
Indexación en MariaDB vs PostgreSQL: por qué las «mejores prácticas» fallan en cargas reales
Siguiente →
Tiempos de espera en contenedores Docker: ajusta los reintentos correctamente (no infinitos)

Deja un comentario