Cómo Diseñar una Estrategia de Caching Multicapa y CDN para Acelerar Tu Aplicación Web a Medida
Cada 100 milisegundos de latencia adicional reducen las conversiones un 7 %, según datos publicados por Akamai en 2023. Para una tienda online española que facture 2 millones de euros al año, eso son 140 000 euros que se evaporan por un problema que, casi siempre, se resuelve con una estrategia de caching bien pensada.
Lo malo es que cachear no es "activar Varnish y a otra cosa". En una aplicación web a medida —donde conviven contenido estático, datos personalizados por usuario y llamadas a APIs de terceros— el caching exige pensar en capas: navegador, servidor de aplicaciones, reverse proxy y CDN trabajando coordinados. Si una capa se equivoca, las demás también.
Llevo más de quince años poniendo cachés delante de aplicaciones y la lección recurrente es esta: el problema nunca es la tecnología, es la coordinación entre piezas. En esta guía recorro las cuatro capas, explico cómo encajarlas y dejo configuraciones reales que puedes copiar tal cual para que tu aplicación web a medida cargue en menos de un segundo para el 90 % de los usuarios.
Por qué hace falta caching multicapa cuando no usas un framework genérico
WordPress y Shopify llegan con la cache medio resuelta. Una aplicación a medida no tiene esos atajos: cada decisión arquitectónica condiciona qué se puede cachear, durante cuánto y dónde.
Un caso que viví de cerca: una plataforma de reservas hotelera en España servía resultados de búsqueda con tiempos medios de 3,2 segundos. Tras implementar caching en tres capas —Redis para las consultas de disponibilidad, un CDN para assets estáticos y cache de navegador con stale-while-revalidate— el tiempo medio bajó a 480 milisegundos. La tasa de rebote cayó un 23 % en el primer mes. No cambiamos ni una línea de la lógica de negocio.
Las ventajas se acumulan, no se solapan:
- Menos carga en origen: cada capa que absorbe peticiones alivia a la siguiente. Un CDN bien configurado se come el 85-95 % del tráfico estático antes de que llegue al servidor.
- Resiliencia ante picos: en Black Friday o en lanzamientos puntuales, las capas intermedias hacen de amortiguador y te ahorran escalar a las prisas.
- Mejor experiencia móvil: el 78 % del tráfico web en España viene de móviles (Statista 2025). En 4G en zona rural la latencia por salto está entre 40 y 80 ms; cada hop que evitas se nota en el cronómetro del usuario.
- Factura más baja: servir contenido cacheado cuesta entre 10x y 100x menos que generarlo dinámicamente. Un servidor que procesa 500 peticiones/segundo puede bajar a 50 si el 90 % se resuelve antes de tocarlo.
Capa 1: cache de navegador, la primera línea de defensa
Es la capa más cercana al usuario y, paradójicamente, la más ignorada en proyectos a medida. Cuando un recurso se sirve desde la cache local del navegador, el tiempo de carga es prácticamente cero: no hay petición HTTP, no hay latencia de red, no hay procesamiento en servidor. Es como tener una nevera en la cocina: lo que ya está ahí no hace falta ir a buscarlo al supermercado.
Las cabeceras que importan
El control se ejerce mediante cabeceras HTTP que el servidor envía en cada respuesta:
Cache-Control: la principal. Acepta directivas comomax-age(segundos que el recurso es válido),public(cacheable por cualquier intermediario),private(solo el navegador del usuario),no-cache(obliga a revalidar) yno-store(no cachear en absoluto).ETag: identificador único del contenido (hash del fichero). Cuando el navegador revalida envía el ETag conIf-None-Match; si coincide, el servidor responde con304 Not Modifiedsin retransmitir el cuerpo.Last-Modified/If-Modified-Since: alternativa temporal al ETag, útil para recursos que cambian con frecuencia predecible.
Qué poner según el tipo de recurso
| Tipo de recurso | Cache-Control recomendado | Razón |
|---|---|---|
| JS/CSS con hash en nombre | public, max-age=31536000, immutable |
El hash garantiza que un cambio genera un nombre nuevo; se puede cachear un año sin riesgo |
| Imágenes de producto | public, max-age=86400, stale-while-revalidate=3600 |
Cambian poco, pero conviene revalidar diariamente |
| HTML de páginas dinámicas | private, no-cache |
Dependen del usuario; el navegador debe revalidar siempre |
| Respuestas de API autenticadas | private, no-store |
Contienen datos personales; no deben persistir en disco |
| Fuentes web (woff2) | public, max-age=31536000, immutable |
No cambian nunca para una versión dada |
Detente un momento en stale-while-revalidate (SWR). Está disponible en todos los navegadores modernos desde 2020 y permite servir el recurso caducado al usuario mientras el navegador revalida en segundo plano. El usuario no percibe el retraso y el contenido se actualiza para la siguiente visita. Es de esas piezas que parecen un detalle y acaban moviendo la aguja.
Errores que veo una y otra vez
Servir Cache-Control: no-cache, no-store, must-revalidate para todo "por seguridad" tira a la basura cualquier beneficio de cache de navegador. El otro fallo clásico: no meter hashes en los nombres de los assets (bundle.js en vez de bundle.a3f7c2.js), lo que te obliga a TTLs cortos para no servir versiones viejas. Si tu bundler no lo hace por defecto, es media tarde de trabajo y se acabó el problema para siempre.
Capa 2: cache del servidor de aplicaciones
Esta capa vive dentro de tu infraestructura, entre la lógica de negocio y el almacenamiento persistente. Su trabajo es evitar lo caro: consultas a base de datos, llamadas a APIs externas, renderizado de plantillas complejas.
Redis como almacén de cache
Redis es el estándar de facto, con tiempos de lectura por debajo del milisegundo en la mayoría de operaciones. En el ecosistema español de desarrollo web, más del 60 % de las aplicaciones a medida con tráfico significativo usan Redis como capa de cache, según encuestas de la comunidad Python España y Node.js Madrid.
Configuración básica para Node.js con Express:
const Redis = require('ioredis');
const redis = new Redis({
host: process.env.REDIS_HOST,
port: 6379,
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 50, 2000)
});
async function getCachedData(key, fetchFn, ttlSeconds = 300) {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const fresh = await fetchFn();
await redis.setex(key, ttlSeconds, JSON.stringify(fresh));
return fresh;
}
Patrones de invalidación
La invalidación de cache es, como dijo Phil Karlton, uno de los dos problemas difíciles de la informática. En aplicaciones a medida hay tres patrones que usarás todo el tiempo:
- TTL (Time to Live): el recurso expira automáticamente tras un tiempo. Simple pero impreciso.
- Invalidación explícita: cuando los datos cambian, el código borra la clave correspondiente en Redis. Preciso pero requiere disciplina.
- Cache-aside con versioning: cada clave incluye un número de versión que se incrementa con cada cambio; las claves antiguas expiran solas por TTL.
La primera vez que invalidé toda la cache de un cliente en producción fue un viernes a las 19:30. Borré el namespace entero "para asegurarme" y el origen aguantó tres minutos antes de empezar a devolver 503. Desde entonces, las invalidaciones masivas se hacen con purga selectiva por tag, nunca con FLUSHDB, y cualquier cosa que toque más del 5 % de las claves pasa por revisión.
Para datos que cambian de forma impredecible (precios en tiempo real, stock), la combinación de TTL corto (15-60 segundos) con stale-while-revalidate a nivel de aplicación es el mejor equilibrio entre frescura y rendimiento.
Cache de consultas de base de datos
En aplicaciones con PostgreSQL o MySQL, las consultas repetitivas son candidatas perfectas. Un patrón que funciona es generar la clave de cache a partir del hash de la consulta SQL y sus parámetros:
import hashlib
import json
def cache_key_for_query(sql, params):
raw = f"{sql}:{json.dumps(params, sort_keys=True)}"
return f"dbcache:{hashlib.sha256(raw.encode()).hexdigest()[:16]}"
En una aplicación de gestión logística que desarrollé para una empresa de transporte en Valencia, este enfoque redujo las consultas a PostgreSQL un 72 %, de 1 200 consultas/segundo a 336 en hora punta. La base de datos pasó del 85 % de CPU al 30 % estable. Lo gracioso: el cuello de botella que tenía al equipo bloqueado desde hacía meses se resolvió en dos días de trabajo.
Capa 3: reverse proxy con Varnish y Nginx
El reverse proxy se sitúa delante del servidor de aplicaciones y cachea respuestas HTTP completas. Es especialmente eficaz para páginas iguales para todos los usuarios (páginas de producto, listados, contenido editorial). Si el navegador es la nevera de la cocina, esta capa es el congelador del sótano: más capacidad, un poco más lejos, pero infinitamente más rápido que ir al hipermercado.
Varnish Cache
Varnish es un acelerador HTTP diseñado específicamente para esto. Opera en memoria RAM y puede servir decenas de miles de peticiones por segundo en hardware modesto. Su lenguaje de configuración (VCL) permite reglas extremadamente granulares.
Una configuración básica pero funcional:
sub vcl_recv {
# No cachear peticiones autenticadas
if (req.http.Authorization || req.http.Cookie ~ "session_id") {
return (pass);
}
# Normalizar Accept-Encoding para mejorar hit ratio
if (req.http.Accept-Encoding) {
if (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} else {
unset req.http.Accept-Encoding;
}
}
}
sub vcl_backend_response {
# Cache por defecto: 5 minutos
if (beresp.ttl <= 0s) {
set beresp.ttl = 300s;
}
# Assets estáticos: 24 horas
if (bereq.url ~ "\.(css|js|jpg|png|webp|woff2)$") {
set beresp.ttl = 86400s;
}
# Grace: servir contenido caducado durante revalidación
set beresp.grace = 1h;
}
La directiva grace en Varnish es el equivalente al stale-while-revalidate del navegador: sirve contenido caducado mientras se refresca en segundo plano. En picos de tráfico esto te salva del "cache stampede" (avalancha de peticiones al origen cuando una clave popular expira). Si tu aplicación tiene "horas punta", no te plantees siquiera no configurarlo.
Nginx como capa de cache
Nginx también vale como cache reverse proxy. Si ya lo usas como balanceador o terminador TLS, añadir caching es sencillo y te ahorra un componente más en la arquitectura:
proxy_cache_path /var/cache/nginx levels=1:2
keys_zone=app_cache:64m
max_size=2g
inactive=24h
use_temp_path=off;
server {
location / {
proxy_cache app_cache;
proxy_cache_valid 200 5m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating;
proxy_cache_background_update on;
add_header X-Cache-Status $upstream_cache_status;
}
}
La cabecera X-Cache-Status es oro puro para depurar: devuelve HIT, MISS, EXPIRED o BYPASS. Cuando alguien te diga "esto no se está cacheando", abre las DevTools y mira esta cabecera antes de pensar nada más.
Capa 4: CDN, geografía aplicada al rendimiento
La CDN (Content Delivery Network) es la capa más externa. Distribuye el contenido en servidores edge repartidos por el mundo, de forma que cada usuario recibe los datos desde el nodo más cercano. Para usuarios en España, un CDN con presencia en Madrid, Barcelona y Lisboa reduce la latencia de red a 5-15 ms frente a los 40-80 ms de un servidor centralizado en Irlanda (región eu-west-1 de AWS). Es la diferencia entre que la respuesta venga del barrio de al lado o de otro país.
Comparativa para el mercado español
| CDN | PoPs en España | Precio base | Punto fuerte |
|---|---|---|---|
| Cloudflare | Madrid, Barcelona | Gratis (plan básico) | Workers para lógica en edge, protección DDoS incluida |
| Fastly | Madrid | Desde 50 USD/mes | Purga instantánea (<150 ms global), VCL nativo |
| AWS CloudFront | Madrid | Pago por uso (~0,085 USD/GB) | Integración nativa con S3, Lambda@Edge |
| Bunny CDN | Madrid, Barcelona | Desde 0,01 USD/GB | Relación calidad-precio, panel simple |
| KeyCDN | Sin PoP en España (Fráncfort) | Desde 0,04 USD/GB | Económico para bajo tráfico |
Para aplicaciones web a medida dirigidas al mercado español, Cloudflare y Bunny CDN son las que mejor relación rendimiento-precio te van a dar. Cloudflare destaca por su plan gratuito (suficiente para muchos proyectos) y por sus Workers, que te dejan ejecutar lógica de negocio en el edge sin tocar el servidor de origen.
Cache de contenido dinámico en el edge
Hace una década, los CDN solo cacheaban contenido estático (imágenes, CSS, JS). Las plataformas modernas permiten cachear también respuestas de API y HTML dinámico mediante funciones en el edge.
Ejemplo típico: una plataforma de e-commerce que genera páginas de categoría filtradas por región. Un Cloudflare Worker puede cachear la respuesta por región y revalidar cada 5 minutos:
export default {
async fetch(request, env) {
const cacheKey = new Request(request.url, {
headers: { 'X-Region': request.cf?.region || 'ES' }
});
const cache = caches.default;
let response = await cache.match(cacheKey);
if (!response) {
response = await fetch(request);
response = new Response(response.body, response);
response.headers.set('Cache-Control', 's-maxage=300');
await cache.put(cacheKey, response.clone());
}
return response;
}
};
Cabeceras específicas para CDN
La interacción entre CDN y las capas anteriores se controla con cabeceras específicas:
s-maxage: tiempo de cache específico para proxies y CDNs, independiente delmax-ageque ve el navegador. Permite, por ejemplo, cachear 5 minutos en CDN pero solo 60 segundos en navegador.Surrogate-Control: cabecera propietaria de Fastly y otros CDNs para control fino (similar as-maxagepero con más opciones).Vary: indica al CDN que debe mantener versiones separadas del recurso según ciertas cabeceras de la petición (por ejemplo,Vary: Accept-Encoding, Accept-Language).Cache-Tag/Surrogate-Key: permite etiquetar respuestas y purgar por etiqueta. Si actualizas un producto, puedes purgar todas las páginas que lo incluyen sin tocar el resto.
Orquestación: que las cuatro capas tiren del mismo lado
El reto del caching multicapa no es configurar cada capa por separado, es asegurar que trabajan en armonía. Un error de coordinación puede provocar datos obsoletos, cache poisoning o, en el peor caso, servir información de un usuario a otro. Y no, no es teórico: lo he visto pasar más de una vez en auditorías.
Jerarquía de TTLs
La regla general es que los TTL deben disminuir conforme te acercas al usuario:
- Redis (servidor): TTL de 5-15 minutos para datos dinámicos
- Varnish/Nginx (reverse proxy): TTL de 1-5 minutos
- CDN: TTL de 30 segundos a 5 minutos (con
stale-while-revalidate) - Navegador:
no-cachepara HTML dinámico, TTL largo solo para assets con hash
Esta jerarquía garantiza que la capa más cercana al usuario tenga datos iguales o más frescos que la exterior. Si el CDN cachea 5 minutos pero Redis cachea 15, puedes acabar sirviendo datos de hasta 20 minutos de antigüedad (15 en Redis + 5 en CDN). Suena obvio escrito, pero es uno de los bugs más comunes que me encuentro en revisiones.
Invalidación en cascada
Cuando un dato cambia, la invalidación debe propagarse de dentro hacia fuera:
- Actualizar la base de datos
- Invalidar la clave en Redis
- Enviar un PURGE al reverse proxy
- Enviar un purge al CDN (por URL o por cache tag)
Herramientas como Fastly permiten purgas globales en menos de 150 milisegundos. Cloudflare garantiza purgas en menos de 30 segundos para la mayoría de los PoPs. Esto hace viable cachear contenido dinámico con TTLs altos y depender de la invalidación activa para mantener la frescura.
Monitorización del rendimiento
Sin métricas, no sabes si tu estrategia funciona, así de simple. Los indicadores que tienes que mirar:
- Hit ratio: porcentaje de peticiones servidas desde cache. Objetivo: >85 % para assets estáticos, >60 % para contenido dinámico cacheable.
- Tiempo hasta primer byte (TTFB): debe ser <200 ms para el percentil 75. Google usa TTFB como señal de ranking desde 2021.
- Tasa de invalidaciones: un número excesivo de purges indica que el TTL es demasiado agresivo o que la lógica de invalidación tiene errores.
Herramientas que uso a diario: Grafana con Prometheus para métricas de Varnish y Redis, el panel de analytics del CDN y Google PageSpeed Insights para validar el impacto en Core Web Vitals.
Implementación paso a paso
Si partes de una aplicación a medida sin ninguna capa de cache, esta es la secuencia que recomiendo para minimizar riesgo y maximizar impacto. Cuatro semanas, una capa por semana, midiendo entre medias:
Semana 1 — Assets estáticos con hash y cache de navegador. Configura tu bundler (Vite, Webpack, esbuild) para generar nombres de archivo con hash de contenido. Añade cabeceras Cache-Control: public, max-age=31536000, immutable para estos ficheros. Impacto esperado: reducción del 30-40 % en peticiones al servidor.
Semana 2 — Redis para consultas frecuentes. Identifica las 10 consultas SQL más ejecutadas (usa pg_stat_statements en PostgreSQL o el slow query log en MySQL). Implementa cache-aside con Redis para esas consultas con TTL de 5 minutos. Impacto esperado: reducción del 50-70 % en carga de base de datos.
Semana 3 — CDN para contenido público. Configura Cloudflare o Bunny CDN delante de tu aplicación. Establece reglas de cache por ruta: assets estáticos con TTL largo, páginas públicas con TTL de 5 minutos y stale-while-revalidate, API autenticada sin cache en CDN. Impacto esperado: TTFB <100 ms para usuarios en España.
Semana 4 — Reverse proxy y ajuste fino. Si el tráfico lo justifica (más de 1 000 peticiones/minuto al origen), añade Varnish o Nginx cache entre el CDN y el servidor de aplicaciones. Configura grace periods y monitoriza hit ratios. Ajusta TTLs basándote en datos reales, no en suposiciones.
Este enfoque incremental permite medir el impacto de cada capa por separado y facilita la depuración cuando algo no funciona como esperabas (y algo siempre falla).
Cuánto cuesta no cachear comparado con hacerlo bien
Una aplicación web a medida que sirve 500 000 páginas vistas al mes sin caching puede necesitar un servidor con 8 vCPU y 32 GB de RAM (coste aproximado en AWS: 280 EUR/mes). Con caching multicapa, esa misma carga se sirve desde una instancia de 2 vCPU y 8 GB (70 EUR/mes) más Cloudflare gratuito y Redis Elasticache básico (25 EUR/mes). El ahorro neto supera los 180 EUR/mes y crece con el tráfico.
Pero el ahorro grande está en las conversiones. Un TTFB por debajo de 200 ms y un LCP inferior a 2,5 segundos sitúan tu aplicación en el rango "bueno" de Core Web Vitals, lo que influye en el posicionamiento orgánico y en la tasa de conversión. Para aplicaciones B2B españolas, donde un lead cualificado cuesta más de 80 EUR, cada décima de segundo cuenta. Si necesitas ayuda para diseñar e implementar una estrategia de caching multicapa adaptada a la arquitectura concreta de tu aplicación web a medida, habla con nuestro equipo técnico y montamos juntos un plan con métricas claras desde la primera semana.