StackPractices
intermediate Por Mathias Paulenko

Estrategias y patrones de invalidación de caché CDN

Implementa invalidación de caché CDN usando purge APIs, surrogate keys, invalidación por tags y URLs versionadas para mantener el contenido fresco

El caché CDN reduce latencia y carga del origen, pero el contenido cacheado puede volverse obsoleto. La invalidación le dice al CDN que obtenga una copia fresca del origen. El siguiente enfoque cubre cuatro estrategias de invalidación — purge por URL, invalidación por surrogate key (tags), URLs versionadas y soft purge — con ejemplos de código para Cloudflare y Fastly.

Cuándo Usar Esto

  • Actualizaciones de contenido que deben aparecer inmediatamente (noticias, precios, inventario)
  • Despliegues donde assets viejos no deben persistir en el edge
  • Sitios multi-página donde actualizar una página debe invalidar páginas relacionadas (un cambio de producto que también afecta su página de categoría y los resultados de búsqueda)
  • Flujos disparados por CMS o webhooks donde el origen sabe exactamente qué cambió y puede lanzar un purge dirigido
  • Páginas de alto tráfico donde un purge duro golpearía el origen con cache misses simultáneos — usa soft purge en su lugar

Puedes saltarte la invalidación por completo si tus assets estáticos ya usan filenames con hash de contenido y tus respuestas HTML llevan TTLs cortos — la URL cambia sola, así que no hay nada que purgar. Si estás cacheando respuestas de API en Redis en lugar del edge, es una capa distinta — ver Caché en Node.js con Redis: Cache-Aside, TTL e Invalidation.

Requisitos Previos

  • Un proveedor CDN (Cloudflare, Fastly, AWS CloudFront o similar)
  • Credenciales de API para el CDN

Solución

1. Purge por URL — invalidar URLs específicas

Cloudflare:

import httpx

CLOUDFLARE_API = "https://api.cloudflare.com/client/v4"

async def purge_cloudflare_urls(zone_id: str, api_token: str, urls: list[str]) -> dict:
    """Purge specific URLs from Cloudflare's cache.

    Args:
        zone_id: Cloudflare zone ID.
        api_token: API token with purge permission.
        urls: List of URLs to purge.

    Returns:
        API response dict.
    """
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{CLOUDFLARE_API}/zones/{zone_id}/purge_cache",
            headers={
                "Authorization": f"Bearer {api_token}",
                "Content-Type": "application/json",
            },
            json={"files": urls},
        )
        return response.json()

# Uso
await purge_cloudflare_urls(
    zone_id="abc123",
    api_token="token",
    urls=["https://example.com/page1", "https://example.com/page2"],
)

Fastly:

FASTLY_API = "https://api.fastly.com"

async def purge_fastly_urls(service_id: str, api_key: str, urls: list[str]) -> list[dict]:
    """Purge specific URLs from Fastly's cache."""
    results = []
    async with httpx.AsyncClient() as client:
        for url in urls:
            response = await client.post(
                f"{FASTLY_API}/purge/{url}",
                headers={"Fastly-Key": api_key},
            )
            results.append(response.json())
    return results

2. Invalidación por surrogate key — purge por tags

Las surrogate keys te permiten etiquetar URLs relacionadas e invalidarlas con una llamada a la API. Esto es ideal cuando actualizar un producto debe invalidar su página de producto, página de categoría y resultados de búsqueda.

Fastly (surrogate keys nativas):

# En los headers de respuesta de tu origen:
# Surrogate-Key: product-123 category-456

async def purge_fastly_key(service_id: str, api_key: str, surrogate_key: str) -> dict:
    """Purge all URLs tagged with a surrogate key."""
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{FASTLY_API}/service/{service_id}/purge/{surrogate_key}",
            headers={"Fastly-Key": api_key},
        )
        return response.json()

# Uso — purgar todas las páginas relacionadas con product-123
await purge_fastly_key("svc123", "key", "product-123")

Cloudflare (Cache Tags):

# En los headers de respuesta de tu origen:
# Cache-Tag: product-123, category-456

async def purge_cloudflare_tags(zone_id: str, api_token: str, tags: list[str]) -> dict:
    """Purge all URLs with matching cache tags."""
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{CLOUDFLARE_API}/zones/{zone_id}/purge_cache",
            headers={
                "Authorization": f"Bearer {api_token}",
                "Content-Type": "application/json",
            },
            json={"tags": tags},
        )
        return response.json()

3. URLs versionadas — invalidación por hash de contenido

En lugar de purgar, cambia la URL cuando el contenido cambia. Esto elimina la invalidación por completo:

// Paso de build — genera URLs de assets con hash de contenido
import crypto from "crypto";
import fs from "fs";

function generateVersionedAsset(filePath: string): string {
  const content = fs.readFileSync(filePath);
  const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
  const ext = filePath.split(".").pop();
  const base = filePath.replace(/\.\w+$/, "");
  return `${base}.${hash}.${ext}`;
}

// app.js -> app.a1b2c3d4.js
// Cuando el contenido cambia, el hash cambia, y el CDN obtiene la nueva URL
<!-- El HTML referencia el filename con hash -->
<script src="/assets/app.a1b2c3d4.js"></script>
<link rel="stylesheet" href="/assets/styles.e5f6g7h8.css" />

4. Soft purge — invalidación elegante

Soft purge marca el contenido cacheado como obsoleto en lugar de eliminarlo. El CDN sirve el contenido obsoleto mientras obtiene una copia fresca en background, evitando picos en el origen.

Fastly Soft Purge:

async def soft_purge_fastly(service_id: str, api_key: str, surrogate_key: str) -> dict:
    """Soft purge — mark as stale, serve while refreshing."""
    async with httpx.AsyncClient() as client:
        response = await client.request(
            "POST",
            f"{FASTLY_API}/service/{service_id}/purge/{surrogate_key}",
            headers={
                "Fastly-Key": api_key,
                "Fastly-Soft-Purge": "1",
                "Fastly-Soft-Purge-TTL": "30",
            },
        )
        return response.json()

5. Invalidación automatizada al desplegar

import os

async def invalidate_on_deploy():
    """Purge CDN cache after a deployment."""
    cdn = os.getenv("CDN_PROVIDER")
    service_id = os.getenv("CDN_SERVICE_ID")
    api_key = os.getenv("CDN_API_KEY")

    if cdn == "fastly":
        # Purgar todo vía surrogate key "all"
        await purge_fastly_key(service_id, api_key, "all")
    elif cdn == "cloudflare":
        zone_id = os.getenv("CLOUDFLARE_ZONE_ID")
        api_token = os.getenv("CLOUDFLARE_API_TOKEN")
        # Purgar todo
        async with httpx.AsyncClient() as client:
            await client.post(
                f"{CLOUDFLARE_API}/zones/{zone_id}/purge_cache",
                headers={"Authorization": f"Bearer {api_token}"},
                json={"purge_everything": True},
            )

    print("CDN cache invalidated after deploy")

Cómo Funciona

  1. Purge por URL envía una llamada a la API al CDN para eliminar URLs específicas de la caché del edge. La siguiente petición obtiene del origen.
  2. Surrogate keys (Fastly) y cache tags (Cloudflare) agrupan URLs por tag. Purgar un tag invalida todas las URLs con ese tag en una llamada, evitando enumerar cada URL.
  3. URLs versionadas eliminan la invalidación — la URL cambia cuando el contenido cambia, por lo que el CDN siempre obtiene el archivo nuevo. Las URLs viejas permanecen cacheadas hasta que su TTL expira.
  4. Soft purge establece el TTL del objeto cacheado a un valor corto (ej. 30 segundos) en lugar de eliminarlo. El CDN sirve contenido obsoleto mientras asíncronamente obtiene una copia fresca.

Elegir una estrategia

flowchart diagram: Contenido cambió

Comparativa de proveedores

Las cuatro estrategias existen en cualquier CDN importante, pero la mecánica difiere lo suficiente como para elegir un modelo de proveedor antes de escribir la integración:

ProveedorMétodo de purgeSoporte de tagsPropagación típicaLímites y coste
Cloudflareendpoint purge_cache (files, prefixes, tags, hosts, purge_everything)header Cache-Tag — el purge por tags requiere plan Enterprise~30–60 s global~30 peticiones de purge/min por zona; incluido en todos los planes
FastlyPOST /purge/{url} o /service/{id}/purge/{key}header Surrogate-Key en todos los planes, soft purge integrado~1–5 s (red de purge instantáneo)Incluido; soft purge evita picos en el origen
AWS CloudFrontAPI CreateInvalidation con patrones de path como /products/*Sin tags nativos — agrupa por prefijo de pathSegundos a minutos por edge locationPrimeros 1,000 paths/mes gratis, luego cobra por path

Dos detalles que muerden en producción: el purge por tags de Cloudflare está limitado al plan Enterprise — en planes inferiores solo puedes purgar por URL, prefijo y host, así que diseña tu taxonomía de tags alrededor de prefijos si no estás en Enterprise. Y CloudFront no soporta tags en absoluto, lo que te empuja hacia URLs versionadas e invalidaciones por path en lugar de agrupación de contenido.

Versiones ejecutables de estos snippets — un cliente de purge que cubre los tres proveedores más un webhook FastAPI — están en el repositorio complementario.

Variantes

Invalidación con AWS CloudFront

import time

import boto3

def invalidate_cloudfront(distribution_id: str, paths: list[str]) -> dict:
    """Create a CloudFront invalidation."""
    client = boto3.client("cloudfront")
    response = client.create_invalidation(
        DistributionId=distribution_id,
        InvalidationBatch={
            "Paths": {
                "Quantity": len(paths),
                "Items": paths,
            },
            "CallerReference": str(int(time.time())),
        },
    )
    return response

Invalidación selectiva por tipo de contenido

async def invalidate_product(
    product_id: str,
    category_id: str,
    zone_id: str,
    api_token: str,
) -> dict:
    """Invalidate all cache entries related to a product."""
    tags = [
        f"product-{product_id}",
        f"category-{category_id}",
        "search-results",
        "product-feed",
    ]
    return await purge_cloudflare_tags(zone_id, api_token, tags)

Webhook de invalidación

import os

from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/webhook/cdn-invalidate")
async def invalidate_webhook(request: Request):
    """Webhook endpoint for CMS-triggered invalidation."""
    payload = await request.json()
    if payload.get("secret") != os.getenv("WEBHOOK_SECRET"):
        raise HTTPException(status_code=403)

    if payload["type"] == "product_updated":
        await invalidate_product(
            payload["product_id"],
            payload["category_id"],
            os.getenv("CLOUDFLARE_ZONE_ID"),
            os.getenv("CLOUDFLARE_API_TOKEN"),
        )
    elif payload["type"] == "full_deploy":
        await invalidate_on_deploy()

    return {"status": "ok"}

Mejores Prácticas

  • Usa surrogate keys para contenido relacionado — purgar una key invalida todas las URLs relacionadas sin enumerarlas
  • Prefiere URLs versionadas para assets estáticos — elimina la necesidad de invalidación por completo
  • Usa soft purge para páginas de alto tráfico — evita picos en el origen por cache misses simultáneos
  • Agrupa peticiones de purge — la mayoría de CDN rate-limitan las llamadas a la API de purge; agrupa URLs o usa tags

Errores Comunes

  • Purgar todo en cada despliegue — causa picos en el origen; usa surrogate keys para invalidación dirigida
  • No establecer headers Surrogate-Key — sin tags, solo puedes purgar por URL, lo que requiere conocer cada URL afectada
  • Purgar en lugar de versionar — para assets estáticos, las URLs versionadas son más simples y confiables
  • No manejar rate limits del API de purge — Cloudflare permite ~30 peticiones de purge por minuto por zona; agrupa en consecuencia

Ver También

Preguntas frecuentes

¿Cuánto tarda la invalidación del CDN?

Típicamente 30–60 segundos para Cloudflare y 1–5 segundos para Fastly. Las invalidaciones de CloudFront tardan más — a menudo unos minutos — porque tienen que llegar a cada edge location. Depende de la velocidad de propagación del edge del CDN, así que no encadenes flujos visibles al usuario a que un purge termine al instante.

¿Debo purgar o usar TTLs cortos?

Usa TTLs cortos (60–300 segundos) para contenido que cambia frecuentemente — se autocorrige sin trabajo de integración. Usa purge para invalidación inmediata cuando el contenido obsoleto es inaceptable, como una corrección de precios o una baja de contenido.

¿Puedo purgar por patrón de URL (ej. /products/*)?

Cloudflare soporta purge por prefijo con "prefixes": ["/products/"]. Fastly usa surrogate keys para el mismo efecto — etiqueta todo lo de /products/ con una key products y purga la key. CloudFront acepta wildcards de path directamente en los paths de invalidación.

¿Cuál es el coste de las llamadas al API de purge del CDN?

Cloudflare incluye purge en todos los planes pero el purge por tags requiere Enterprise. Fastly incluye el purge con rate limits por plan. AWS CloudFront te da 1,000 paths de invalidación gratis al mes y cobra por path después — los paths con wildcard cuentan como uno, así que /products/* es barato pero /products/item-1 hasta /products/item-900 sale caro.

¿Cómo confirmo que un purge realmente funcionó?

Pide la URL dos veces con curl -I. Justo después del purge deberías ver un cache miss — CF-Cache-Status: MISS en Cloudflare o X-Cache: MISS en Fastly, normalmente con Age: 0. La segunda petición debería dar HIT otra vez con el contenido fresco. Si la primera sigue devolviendo HIT, el purge todavía no propagó a ese edge — espera unos segundos y reintenta.

¿Qué pasa cuando miles de objetos cacheados expiran a la vez?

Tienes un origin stampede: todos los nodos edge fallan a la vez y golpean el origen juntos. Evítalo con soft purge para que los nodos sirvan stale mientras revalidan, añade jitter a los TTLs para que las expiraciones se repartan, y agrupa los purges grandes en lugar de lanzarlos todos de golpe.

¿Cuál es la diferencia entre purge y ban en caché de CDN?

Purge elimina el contenido cacheado inmediatamente y la próxima petición obtiene una copia fresca del origen. Ban marca el contenido como stale pero lo deja en caché — la próxima petición dispara una revalidación. Usa purge para limpieza inmediata. Usa ban cuando quieres invalidación lazy para reducir picos de carga en el origen.

¿Cómo hago cache-bust sin cambiar la URL?

No puedes. El caché de navegador y CDN está basado en URL. Para forzar un refresh, debes cambiar la URL — ya sea el filename (content hash), un query parameter (?v=2), o un path prefix (/v2/asset.css). El content hashing (ej. [name].[contenthash].js) es el enfoque más confiable porque el hash cambia solo cuando el contenido del archivo cambia.