StackPractices
beginner Por Mathias Paulenko

Patrón Content Delivery Network (CDN)

Distribuye contenido estático y en vivo a través de servidores edge geográficamente dispersos para reducir latencia, mejorar disponibilidad y descargar infraestructura de origen.

Descripción General

Una Content Delivery Network coloca copias cacheadas de tu contenido en servidores repartidos por el mundo, de modo que cada request la responde la ubicación edge más cercana al usuario. La ganancia de velocidad es sobre todo física: un usuario en Tokio que pega contra un origen en Virginia paga unos 150-200 ms de latencia de ida y vuelta por request, mientras que un nodo edge en Tokio responde en unos 20 ms. Multiplica eso por las decenas de requests que necesita una página y la diferencia deja de ser sutil.

Más allá de la distancia, un CDN absorbe tráfico de lectura para que tu origen no tenga que hacerlo, amortigua picos y ataques DDoS, y cada vez más ejecuta código en el edge (Cloudflare Workers, Lambda@Edge, Fastly Compute). Lo que empezó como “cache para imágenes” hoy cubre terminación TLS, filtrado de bots, geo-routing y, para algunas apps, el manejo completo de requests.

flowchart diagram: U([Usuario

Cuándo Usar

Usa el Patrón CDN cuando:

  • Los usuarios están geográficamente distribuidos y la latencia importa
  • Los assets estáticos (imágenes, CSS, JS, fuentes, videos) representan la mayor parte del tráfico. Esto combina de forma natural con el Patrón de Static Content Hosting
  • Necesitas absorber picos de tráfico sin escalar la infraestructura de origen
  • Se requiere protección DDoS o funcionalidad WAF en el edge
  • El edge computing para A/B testing, geo-routing o autenticación te da algo que el origen no puede

Si tu problema es el volumen de requests y no la distancia, como un cliente machacando tu API, el Patrón de Throttling encaja mejor.

Cuándo Evitar

  • Los usuarios están en la misma región que el origen. Si todos están ya a 20 ms del servidor, el salto extra por el edge no compra nada.
  • El contenido no se puede cachear y dominan los datos por usuario. Los payloads personalizados aún ganan por la descarga de TLS, pero pierdes el beneficio principal.
  • Todo es interno. Un CDN privado delante de una intranet casi nunca justifica el costo.
  • La complejidad de invalidación supera el beneficio de latencia. Publicar decenas de páginas personalizadas por segundo puede convertir el purgado de cache en un problema de ingeniería propio.
  • La terminación TLS en el edge viola tus reglas de compliance; raro, pero real en algunas industrias reguladas.

Solución

Un despliegue de CDN tiene tres piezas móviles: la distribución que se pone delante del origen, los headers cache-control que le dicen al edge qué guardar, y la ruta de invalidación para cuando el contenido cambia. Los ejemplos de abajo cubren las tres.

JavaScript (CloudFront + S3 Origin)

// Configuración de AWS SDK v3 para invalidación de CDN
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';

const cloudfront = new CloudFrontClient({ region: 'us-east-1' });

/**
 * Invalida el cache del CDN para paths específicos después de un deployment
 */
async function invalidateCache(distributionId, paths = ['/*']) {
  const command = new CreateInvalidationCommand({
    DistributionId: distributionId,
    InvalidationBatch: {
      CallerReference: Date.now().toString(),
      Paths: {
        Quantity: paths.length,
        Items: paths
      }
    }
  });

  const response = await cloudfront.send(command);
  console.log(`Invalidación creada: ${response.Invalidation.Id}`);
  return response.Invalidation.Id;
}

// Uso: invalidar después de desplegar assets estáticos
await invalidateCache('E1234567890ABC', ['/assets/*', '/index.html']);

Python (CDN Cache Warmup + Edge Logic)

import requests
import hashlib
from typing import List, Dict
from concurrent.futures import ThreadPoolExecutor

class CDNManager:
    """Gestiona interacciones con el CDN: warmup de cache, purgado y health checks"""
    def __init__(self, base_url: str, api_key: str = None):
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key
        self.edge_locations = [
            "us-east", "us-west", "eu-west", "eu-central",
            "ap-southeast", "ap-northeast", "sa-east"
        ]

    def generate_cache_key(self, path: str, params: Dict = None) -> str:
        """Genera una clave de cache determinística para una URL"""
        content = f"{path}:{sorted(params.items()) if params else ''}"
        return hashlib.sha256(content.encode()).hexdigest()[:16]

    def warmup_cache(self, paths: List[str]) -> Dict[str, bool]:
        """Pre-puebla los caches edge del CDN solicitando desde cada ubicación"""
        results = {}

        def fetch(path):
            try:
                response = requests.get(
                    f"{self.base_url}{path}",
                    headers={"X-Cache-Warmup": "true"},
                    timeout=30
                )
                return path, response.status_code == 200
            except Exception:
                return path, False

        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = [executor.submit(fetch, p) for p in paths]
            for future in futures:
                path, success = future.result()
                results[path] = success

        return results

    def purge_cache(self, path: str) -> bool:
        """Purga un path específico del cache del CDN"""
        # La implementación varía según el proveedor (Fastly, Cloudflare, etc.)
        # Este ejemplo usa un endpoint de purge genérico
        try:
            response = requests.post(
                f"{self.base_url}/__purge",
                json={"path": path},
                headers={"Authorization": f"Bearer {self.api_key}"} if self.api_key else {}
            )
            return response.status_code == 200
        except Exception as e:
            print(f"Purge falló para {path}: {e}")
            return False

    def get_cache_status(self, path: str) -> Dict:
        """Verifica el estado de cache HIT/MISS para un path"""
        response = requests.get(f"{self.base_url}{path}")
        return {
            "path": path,
            "cache_status": response.headers.get("X-Cache", "unknown"),
            "age": response.headers.get("Age", "0"),
            "ttl_remaining": response.headers.get("Cache-Control", "")
        }


# Uso
cdn = CDNManager("https://cdn.example.com", api_key="secret-key")

# Warmup de paths críticos después del deployment
warmup_results = cdn.warmup_cache([
    "/assets/main.css",
    "/assets/app.js",
    "/api/config"
])
print(f"Cache warmup: {sum(warmup_results.values())}/{len(warmup_results)} exitosos")

# Verificar estado del cache
status = cdn.get_cache_status("/assets/main.css")
print(f"Cache: {status['cache_status']}, Age: {status['age']}s")

Terraform (AWS CloudFront Distribution)

# Infrastructure as Code para distribución CloudFront CDN
resource "aws_cloudfront_distribution" "cdn" {
  enabled             = true
  is_ipv6_enabled     = true
  comment             = "StackPractices CDN"
  default_root_object = "index.html"
  price_class         = "PriceClass_All" # Distribución global

  # Origen: bucket S3 para assets estáticos
  origin {
    domain_name = aws_s3_bucket.assets.bucket_regional_domain_name
    origin_id   = "S3-assets"

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.oai.cloudfront_access_identity_path
    }
  }

  # Origen: ALB para API dinámica
  origin {
    domain_name = aws_lb.main.dns_name
    origin_id   = "ALB-api"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  # Comportamiento por defecto: assets estáticos desde S3
  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "S3-assets"
    compress         = true

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 86400    # 1 día
    max_ttl                = 31536000 # 1 año
  }

  # Comportamiento ordenado: llamadas API sin caching
  ordered_cache_behavior {
    path_pattern     = "/api/*"
    allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "ALB-api"
    compress         = true

    forwarded_values {
      query_string = true
      headers      = ["Authorization", "Origin"]
      cookies { forward = "all" }
    }

    viewer_protocol_policy = "https-only"
    min_ttl                = 0
    default_ttl            = 0
    max_ttl                = 0
  }

  # Edge caching para tipos de asset específicos
  ordered_cache_behavior {
    path_pattern     = "/assets/*"
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "S3-assets"
    compress         = true

    forwarded_values {
      query_string = true
      cookies { forward = "none" }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 86400    # 1 día
    default_ttl            = 604800    # 1 semana
    max_ttl                = 31536000  # 1 año
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.cdn.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  tags = {
    Environment = "production"
    Service     = "cdn"
  }
}

Headers Cache-Control

La infraestructura por sí sola no es el patrón: los headers que envía tu origen son el contrato que hace que el edge cache funcione:

# Assets estáticos con fingerprint - seguros de cachear "para siempre"
Cache-Control: public, max-age=31536000, immutable

# Páginas HTML - el navegador revalida, pero el CDN puede guardar una copia corta
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=60

# Respuestas personalizadas - nunca almacenar en el edge
Cache-Control: private, no-store

max-age habla con el cliente, mientras que s-maxage habla con los caches compartidos: el CDN. El truco clásico es max-age=0, s-maxage=300: el navegador siempre revalida para que el usuario vea HTML fresco, pero el edge guarda una copia de cinco minutos que absorbe el tráfico repetido. stale-while-revalidate va un paso más y permite que el edge sirva la copia obsoleta mientras la refresca en segundo plano, así que un origen lento nunca llega al usuario.

Explicación

Cómo fluye un request en realidad importa para todo lo que sigue. En modo pull (el default en CloudFront, Cloudflare y similares), el enrutamiento por DNS o anycast manda al usuario al punto de presencia más cercano. El edge busca el request en su cache: con HIT responde en milisegundos; con MISS lo reenvía aguas arriba (al shield regional si hay uno configurado, si no directo a tu origen) y luego guarda la respuesta según el Cache-Control y se la sirve gratis al siguiente request idéntico.

La cache key decide tu hit ratio. El CDN hashea la URL más los inputs del request que tú pongas en whitelist (parámetros de query, headers, cookies) para decidir si dos requests son “el mismo objeto”. Por defecto, no pongas nada en whitelist. Cada input extra multiplica las variantes cacheadas, y parámetros como ?utm_source=newsletter son el asesino clásico del hit ratio: quita los parámetros de tracking en el edge o fragmentarán tu cache en miles de entradas de un solo uso.

Tres temporizadores cooperan en la frescura. max-age es para navegadores, s-maxage es para caches compartidos, y stale-while-revalidate / stale-if-error compran resiliencia cuando el origen tiene problemas. La configuración de TTL del lado del CDN es un fallback para cuando el origen no manda headers, pero los headers del origen deberían ser la fuente de verdad, porque versionan con tus deploys en lugar de quedarse en un dashboard. La Guía Completa de Estrategia de Caching en CDN cubre el diseño de TTLs y claves en profundidad.

Planifica la invalidación antes de necesitarla. Las URLs con fingerprint (app.a3f8b2.js) son la respuesta limpia, porque la URL cambia cuando cambia el contenido, así que no hay nada que purgar. Para URLs estables, llama a la API de purge del proveedor desde el pipeline de deploy; las invalidaciones por path o wildcard se propagan en segundos o minutos según el vendor. El purgado por tags (surrogate keys de Fastly, cache tags de Cloudflare) es la opción precisa cuando muchos objetos expiran a la vez.

El origin shielding colapsa las estampidas. Sin él, cincuenta PoPs fallan a la vez en un deploy y tu origen se come cincuenta fetches concurrentes. Una capa shield designada lo convierte en un solo fetch, que es también por qué el shielding combina bien con health checks sobre el propio origen.

Variantes

VarianteCaso de UsoEjemplo
CDN de assets estáticosImágenes, CSS, JS, fuentesCloudFront + S3
Aceleración de edgeRespuestas de API, páginas HTMLCloudflare Argo, Fastly
Video streamingSegmentos HLS/DASH, streams en vivoAWS MediaPackage, Akamai
Edge computingA/B testing, auth, personalizaciónCloudflare Workers, Lambda@Edge
Multi-CDNResiliencia y optimización de costosCloudFront + Fastly failover

Lo que funciona

  • Pon fingerprint a los assets y cachealos para siempre. app.a3f8b2.js con max-age=31536000, immutable significa que cada deploy tiene una URL nueva y nunca purgas. Este único hábito elimina la mayoría del dolor de invalidación.
  • Ajusta el TTL a la volatilidad, no a un default. Assets versionados: un año; APIs: segundos según tolerancia a datos viejos; HTML: un s-maxage corto con revalidación.
  • Mantén la cache key mínima. Pon query params, headers y cookies en whitelist de forma deliberada. Cada input que añades multiplica variantes y arrastra el hit ratio hacia abajo.
  • Planifica el fallback antes del lanzamiento. El origen tiene que mantenerse sano para servir si el CDN falla o muere: monitóralo directamente, no a través del CDN.
  • Mira el cache-hit ratio, no solo la latencia. Por debajo de ~80% casi siempre es un problema de clave o TTL, no de tráfico. Fastly y Cloudflare lo exponen por path.

Errores Comunes

  • Olvidar la ruta de invalidación. El bug clásico post-deploy: el HTML nuevo referencia app.v2.js pero el CDN sigue sirviendo el HTML viejo porque nadie lo purgó. Automatiza el purge en el pipeline de deploy.
  • Cachear páginas personalizadas públicamente. Sin private/no-store, el dashboard de un usuario puede servirse al siguiente. La mayoría de CDNs saltan el cache al ver Set-Cookie o Authorization; verifica que el tuyo lo haga.
  • Dejar que los parámetros de tracking fragmenten el cache. utm_*, fbclid, gclid crean cada uno una entrada nueva para contenido idéntico. Quítalos en el edge.
  • Comprimir solo en el origen. Activa compresión en el edge (Brotli donde el cliente lo soporte) y deja que el CDN negocie por cliente.
  • Tratar el CDN como un proxy. Si cada request es MISS, has construido un salto extra con factura incluida. Un hit ratio sano es lo que hace el trabajo.

Ejemplos del Mundo Real

Netflix Open Connect

Netflix opera sus propios appliances CDN dentro de redes de ISPs. Poner el contenido dentro de la infraestructura del propio ISP reduce costos de tránsito y entrega streams 4K con buffering mínimo. Es un recordatorio de que a suficiente escala, “usar un CDN” se convierte en “ser el CDN”.

StackPractices.com

Este sitio corre sobre GitHub Pages con Cloudflare delante. El HTML, CSS y JS estáticos se cachean en el edge de Cloudflare, lo que mantiene la carga del origen casi en cero y los tiempos de carga consistentes en todo el mundo.

Shopify Storefronts

Shopify sirve millones de storefronts a través de Fastly. Los assets de tema cacheados en ubicaciones edge entregan cargas de página sub-segundo a nivel global aunque la plataforma origen esté centralizada; el edge caching es lo que hace viable esa arquitectura en primer lugar.

Ver también

Preguntas frecuentes

¿Un CDN solo funciona para contenido estático?

No. Eso era cierto en la era temprana de Akamai. Los CDNs modernos aceleran respuestas en vivo terminando TLS más cerca del usuario, manteniendo conexiones calientes hacia tu origen, y enrutando por enlaces de backbone privados en lugar del internet público. Un cache miss sigue costando un viaje de ida y vuelta, pero es uno más rápido. El edge computing va más lejos y puede responder sin llamar al origen en absoluto.

¿Cómo manejo la invalidación de caché?

La mejor invalidación es la que nunca haces: pon fingerprint a los assets estáticos (app.a3f8b2.js) y la URL cambia cuando cambia el contenido. Para URLs estables, llama a la API de purge del CDN desde tu pipeline de deploy (por path o wildcard). El purgado por tags (surrogate keys de Fastly, cache tags de Cloudflare) es la opción precisa cuando muchos objetos relacionados expiran a la vez.

¿Cuál es la diferencia entre CDN pull y push?

Un CDN pull trae el contenido de tu origen en el primer request y cachea el resultado, así que hay cero trabajo inicial, y solo el primer visitante de cada ubicación edge paga la latencia del origen. Un CDN push te obliga a subir el contenido tú mismo; solo vale la pena para archivos muy grandes o contenido que casi no cambia. Casi todo CDN moderno es pull con origin shielding opcional.

¿Qué diferencia hay entre max-age y s-maxage?

max-age aplica al cliente; s-maxage aplica a los caches compartidos como el CDN y gana cuando ambos están presentes. Cache-Control: max-age=0, s-maxage=300 significa "navegador, revalida siempre; edge, guarda esto cinco minutos". Ese es el truco estándar para HTML que debe mantenerse fresco para el usuario pero cacheable en el edge.

¿Qué pasa si el CDN se cae?

Menos de lo que temes, más de lo que esperas. Los PoPs son masivamente redundantes, pero las caídas completas del proveedor pasan: la caída de Fastly en junio de 2021 dejó fuera una buena parte de la web durante una hora. Mitigaciones: un origen sano para servir directamente, failover por DNS o health checks, y para los realmente paranoicos, un segundo CDN en modo multi-CDN.

¿Debería usar un CDN para una aplicación interna?

Normalmente no. Si los usuarios están en una red corporativa a pocos saltos del origen, el edge añade un intermediario sin ganancia de latencia. Las excepciones: oficinas repartidas por continentes, o usar el edge para terminar TLS y auth antes de que el tráfico llegue a tu red.