StackPractices
intermediate Por Mathias Paulenko

CDN Edge Caching: Cloudflare, CloudFront, Fastly

Configura reglas de edge caching en Cloudflare, AWS CloudFront y Fastly: políticas de TTL, claves de caché, purga y stale-while-revalidate.

Visión General

Tu aplicación sirve usuarios en tres continentes desde un único origen en us-east-1, y todo el que está fuera de EE. UU. espera más de 300 ms por cada asset. Una CDN coloca copias en caché en nodos edge cercanos a cada usuario, así que los archivos estáticos de una página dejan de cruzar el Atlántico en cada petición. Esta receta configura los tres proveedores más habituales: cache rules de Cloudflare, cache policies de CloudFront vía Terraform y VCL de Fastly — más los patrones de purga que mantienen el caché honesto.

Cuándo Usar

Usa este recurso cuando:

  • Los assets estáticos (JS, CSS, fuentes, imágenes) se sirven a una audiencia distribuida geográficamente
  • El origen gasta CPU re-renderizando respuestas idénticas para tráfico anónimo
  • Las respuestas de API son cacheables y se benefician del caché edge con s-maxage
  • Necesitas descargas cacheables cerca de los usuarios para reducir costos de egress

Omite el edge caching cuando:

  • Tu audiencia está concentrada en una región cercana al origen: el salto extra no aporta nada
  • Las respuestas son por usuario y no compartibles (saldos bancarios, páginas de sesión): usa Cache-Control: private y sáltate la CDN en esas rutas
  • Estás en un plan gratuito con mucho tráfico; el ahorro de ancho de banda existe, pero las funciones de pago (claves de caché personalizadas, purga por tags) pueden costar más que el egress del origen

Requisitos

  • Una cuenta de CDN: zona de Cloudflare, cuenta de AWS para CloudFront, o un servicio de Fastly
  • Un token de API con permisos sobre cache rules (Cloudflare) o permisos IAM para CloudFront
  • Opcional: Terraform 1.5+ para el ejemplo de CloudFront; AWS CLI para las invalidaciones
  • Un navegador o curl -I para verificar los headers de respuesta (Age, CF-Cache-Status, X-Cache)

Código companion: el repo companion de cdn-edge-caching tiene el módulo Terraform, el JSON de cache rules, el archivo VCL y los scripts de purga listos para adaptar.

Solución

Cloudflare: cache rules vía la Ruleset API

Las Page Rules quedaron deprecadas en favor de las cache rules del Ruleset Engine. Una sola llamada a rulesets puede crear varias reglas a la vez:

curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "static-asset-caching",
    "kind": "zone",
    "phase": "http_request_cache_settings",
    "rules": [{
      "expression": "(http.request.uri.path.extension in {\"css\" \"js\" \"png\" \"jpg\" \"woff2\"})",
      "action": "set_cache_settings",
      "action_parameters": {
        "edge_ttl": { "mode": "override_origin", "default": 2592000 },
        "browser_ttl": { "mode": "override_origin", "default": 86400 }
      },
      "enabled": true
    }]
  }'

edge_ttl controla cuánto tiempo conserva el objeto Cloudflare; browser_ttl lo que le indica al navegador. Ambos usan override_origin porque las URLs hasheadas hacen seguro un TTL edge de 30 días, porque un nuevo despliegue trae URLs nuevas.

AWS CloudFront: cache policy con Terraform

resource "aws_cloudfront_distribution" "cdn" {
  enabled = true

  origin {
    domain_name = aws_s3_bucket.static.bucket_regional_domain_name
    origin_id   = "S3Origin"
  }

  default_cache_behavior {
    target_origin_id       = "S3Origin"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    compress               = true
    cache_policy_id        = aws_cloudfront_cache_policy.static.id
  }
}

resource "aws_cloudfront_cache_policy" "static" {
  name        = "static-assets"
  min_ttl     = 1
  default_ttl = 86400
  max_ttl     = 31536000

  parameters_in_cache_key {
    headers_config      { header_behavior = "none" }
    cookies_config      { cookie_behavior = "none" }
    query_strings_config { query_string_behavior = "none" }
  }
}

El bloque parameters_in_cache_key es la parte importante: mantener headers, cookies y query strings fuera de la clave de caché es lo que hace que la distribución realmente cachee. Cada parámetro añadido a la clave multiplica el número de entradas por asset.

Fastly: VCL

sub vcl_recv {
  # Marca los assets estáticos hasheados para que vcl_fetch los trate como inmutables
  if (req.url.ext ~ "^(css|js|png|jpg|woff2)$") {
    set req.http.X-Static = "true";
  }
}

sub vcl_fetch {
  if (req.http.X-Static == "true") {
    set beresp.ttl = 365d;
    set beresp.http.Cache-Control = "public, max-age=31536000, immutable";
    set beresp.http.Surrogate-Key = "static";
  }
}

Surrogate-Key agrupa objetos para la purga: PURGE /service/<id>/purge/static vacía todo lo etiquetado como static sin tocar el resto del caché.

Explicación

Toda CDN se reduce al mismo flujo:

flowchart diagram: Usuario

Un cache hit sirve la respuesta desde el PoP sin tocar tu origen — de ahí vienen la reducción de latencia y la descarga del servidor. Un cache miss pide al origen, guarda la respuesta según la cache policy y la sirve. El TTL decide cuánto tiempo el edge confía en esa copia antes de revalidar.

La clave de caché es donde se gana o se pierde el hit ratio. Si tu clave incluye ?utm_source=twitter, cada enlace de marketing se convierte en una entrada aparte y el hit ratio se desploma; la policy de CloudFront anterior elimina los query strings justo por eso. Si sí necesitas respuestas que varíen, añade solo el parámetro concreto (?v=) a la clave en lugar de aceptarlos todos.

Para contenido en vivo, Cache-Control se divide en dos TTLs: max-age para el navegador y s-maxage para la CDN. Cache-Control: max-age=0, s-maxage=60 le dice a los navegadores “revalida siempre” mientras el edge conserva la respuesta un minuto — suficiente para absorber un pico, tan corto que la obsolescencia es invisible. stale-while-revalidate va más allá: sirve la copia obsoleta al instante mientras el edge la refresca en segundo plano.

La mecánica profunda de cache-control está en headers HTTP Cache-Control, y las estrategias de purga y tags se cubren en invalidación de caché en CDN.

Uso Avanzado

Purgar en lugar de esperar

Cuando el contenido debe cambiar antes de que expire su TTL:

# CloudFront: invalidar por ruta
aws cloudfront create-invalidation \
  --distribution-id E1EXAMPLE \
  --paths "/assets/*"

# Cloudflare: purgar por URL
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"files": ["https://example.com/app.js"]}'

# Fastly: purgar por surrogate key
curl -X POST "https://api.fastly.com/service/$SERVICE_ID/purge/static" \
  -H "Fastly-Key: $FASTLY_API_TOKEN"

La purga por surrogate key de Fastly es instantánea y acotada; la purga por tags de Cloudflare funciona igual pero requiere plan Enterprise; en otros planes purgas por URL o por host. Las invalidaciones de CloudFront se propagan a todos los nodos edge en unos minutos y se cobran pasados los 1000 paths gratuitos al mes, por eso los nombres de archivo versionados siguen siendo más baratos que invalidar.

Servir obsoleto cuando el origen cae

stale-while-revalidate y stale-if-error permiten al edge servir una copia expirada en lugar de dar error:

Cache-Control: max-age=60, s-maxage=300, stale-while-revalidate=86400, stale-if-error=604800

Con ese header, una caída del origen se degrada a “los usuarios ven contenido de hasta una semana” en lugar de errores 500: mejora de disponibilidad gratis para contenido casi estático.

Variantes

ProveedorConfiguraciónControl de clave de cachéModelo de purga
CloudflareDashboard, Ruleset API, TerraformCache rules, claves personalizadasURL, tag (Enterprise), host
AWS CloudFrontConsole, Terraform, CDKCache policies + origin request policiesInvalidaciones por ruta
FastlyVCL, API, TerraformControl total vía VCLPurga instantánea por key/URL/surrogate
AkamaiControl Center, PAPIReglas de Property ManagerFast Purge API, cache tags

Lo Que Funciona

  1. Versiona los nombres de archivo de los assets (app.v2.js, style.a1b2c3.css) para que la propia URL sea la clave de caché, así que los despliegues nuevos nunca pelean con entradas viejas
  2. Mantén la clave de caché mínima: headers, cookies y query strings en la clave fragmentan el caché y matan el hit ratio
  3. max-age=0, s-maxage=N para contenido seguro en el edge pero no en el navegador
  4. stale-while-revalidate para que los usuarios nunca esperen un refresh, y stale-if-error para caídas del origen
  5. Monitorea el hit ratio (CF-Cache-Status de Cloudflare, X-Cache de CloudFront, Fastly-Debug de Fastly) y trata las caídas como mala configuración, no como ruido

Errores Comunes

  1. Cachear respuestas que llevan Set-Cookie: una cookie de sesión servida al usuario equivocado es una fuga de datos, no una ganancia de rendimiento
  2. Purgas completas en cada despliegue: vacían todos los edges a la vez; los nombres versionados y las purgas por tag lo evitan
  3. Ignorar los query strings en la clave de caché (?utm_source=... crea miles de entradas duplicadas)
  4. max-age largo en el HTML que referencia assets versionados; el HTML es el puntero mutable, dale un TTL corto
  5. Asumir que la CDN cachea POST: solo GET/HEAD son cacheables en las políticas por defecto

Lectura Adicional

Preguntas frecuentes

¿Cómo cacheo respuestas de API en el edge?

Dales s-maxage para que la CDN cachee mientras los navegadores no (Cache-Control: max-age=0, s-maxage=60), y asegúrate de que la clave de caché incluya lo que varía la respuesta — normalmente la ruta más los query params concretos. Las respuestas con Set-Cookie o Authorization se omiten en las políticas por defecto, que suele ser lo que quieres.

¿Cuál es la diferencia entre purgar e invalidar?

Misma idea, distinto verbo: ambos marcan objetos cacheados como inutilizables para que la siguiente petición los reobtenga. Fastly lo llama purge (instantáneo), CloudFront invalidación (se propaga en minutos y se factura por ruta). Si la URL cambió (nombres versionados) no necesitas ninguno.

¿Merece la pena el edge caching para backends de solo API?

Sí cuando las respuestas son compartidas y cacheables: catálogos de producto, configuraciones públicas, estadísticas agregadas. Es inútil para respuestas por usuario, que deben llevar Cache-Control: private. GraphQL es más difícil que REST aquí porque todo es un POST a un solo endpoint; necesitarías lógica en el edge para normalizar las queries en claves de caché.

¿Cuánto deben durar mis TTLs?

Tanto como la URL siga siendo válida. Los nombres hasheados (app.a1b2c3.js) justifican immutable + TTLs de un año porque un cambio de contenido produce una URL nueva. El HTML y los datos vivos deben ser cortos — de segundos a un minuto en el edge — porque no puedes invalidar lo que ya tiene el navegador.

¿Puedo cachear contenido autenticado o personalizado?

No por defecto, y eso es protector. Si realmente lo necesitas, incluye la dimensión de autenticación en la clave de caché (Vary: Authorization, o un worker en el edge que varíe por una clave derivada de sesión) para que usuarios distintos nunca compartan una entrada. Mal configurado, es un vector de fuga de datos.