Ahorrando un 94% en Costos (Costes) Serverless: Reemplazando AWS API Gateway con Cloudflare Workers

Reduce los costos (costes) serverless en un 94% reemplazando AWS API Gateway con Cloudflare Workers. Consulta el código, la arquitectura y el desglose de costos (costes) para apps de alta escala.

Ahorrando un 94% en Costos (Costes) Serverless: Reemplazando AWS API Gateway con Cloudflare Workers

Si despliegas funciones serverless en AWS, probablemente conozcas la rutina: levantas una función Lambda, colocas un Amazon API Gateway delante de ella y te olvidas de que existe. Es el estándar de la industria: confiable (fiable), fácil de configurar y de “pago por uso”.

Pero el “pago por uso” es un arma de doble filo. Cuando tu aplicación tiene un éxito más allá de las expectativas, esos costos (costes) pueden dispararse fuera de control. Recientemente, mientras planificaba un despliegue de Laravel, audité los costos (costes) proyectados. Me niego a dar por sentadas las configuraciones de infraestructura por defecto; solo porque un servicio sea la opción estándar no significa que sea eficiente para cada etapa de crecimiento.

Quería evitar convertirme en otra entrada en el archivo de Historias de Terror Serverless. Mi objetivo era simple: alto rendimiento y armonía financiera. En este post, compartiré cómo cambié AWS API Gateway por Cloudflare Workers, logrando una reducción de costos (costes) del 94% en escenarios de alto tráfico sin sacrificar la velocidad.

El Problema: El Alto Costo (Coste) de la Escala

AWS API Gateway actúa como el portero de tus funciones serverless. Gestiona el tráfico, maneja el throttling y enruta las peticiones. Para startups que están buscando su product-market fit, esto es excelente porque el tiempo de configuración es mínimo.

Sin embargo, el modelo de precios castiga la escala. En mi última revisión en la región US East (N. Virginia), pagas aproximadamente $1.00 por millón de peticiones para HTTP APIs y significativamente más para REST APIs. Aunque esto suena barato inicialmente, la factura se acumula agresivamente a medida que creces.

Las Matemáticas

Para visualizar el impacto financiero, modelé los costos (costes) para dos escenarios: una escala moderada de 1 millón de peticiones por mes y un objetivo de alta escala de 1.000 millones de peticiones por mes (asumiendo un tamaño promedio de petición de 100 KB).

Escenario 1: 1 Millón de Peticiones/Mes

ServicioCosto Mensual (USD)
AWS HTTP API~$1.00
AWS REST API~$3.50

Escenario 2: 1.000 Millones de Peticiones/Mes

ServicioCosto Mensual (USD)
AWS HTTP API~$930.00
AWS REST API~$3,033.10

Para sitios web de bajo tráfico, el costo (coste) es insignificante. Sin embargo, con 1.000 millones de peticiones, la diferencia es asombrosa. Pagar más de $3,000 al mes simplemente por enrutar tráfico a un servicio de cómputo se sentía excesivo. Este “impuesto de enrutamiento” amenazaba con eclipsar el costo (coste) del cómputo real de Lambda. Necesitaba una alternativa que priorizara la eficiencia sin introducir una infraestructura compleja.

La Solución: Cloudflare Workers como Proxy

La solución se volvió clara cuando analicé Cloudflare Workers. A diferencia de AWS API Gateway, la plataforma de Cloudflare está diseñada para edge computing de alto volumen con una curva de precios mucho más amigable para volúmenes masivos de peticiones.

Me di cuenta de que un Cloudflare Worker podía actuar como un proxy ligero y programable situado frente a la AWS Lambda Function URL. En lugar de pagar a AWS para enrutar el tráfico, podía utilizar la red global de Cloudflare para manejar las peticiones entrantes, aplicar middleware (como CORS, Autenticación o Headers) y reenviar la petición directamente a la Lambda Function URL.

Por qué funciona este enfoque:

  • Menor Latencia: Cloudflare se ejecuta en el edge (borde), terminando la conexión inicial mucho más cerca del usuario.
  • Más Barato a Escala: El precio de Cloudflare por millón de peticiones es significativamente más bajo, y su plan estándar incluye un generoso paquete de peticiones.
  • Flexibilidad: No estamos atados a la configuración de AWS. Usé Hono, un framework web que maneja el enrutamiento y las cabeceras (headers) sin problemas.

Implementación

Aproveché TypeScript y Hono para construir el worker. Esta configuración nos permite manejar archivos (ficheros) estáticos, cabeceras de caché y hacer proxy de las peticiones sin esfuerzo.

1. La Lógica del Proxy

Definimos un manejador de middleware que intercepta la petición y la reenvía a la LAMBDA_FUNCTION_URL. El ingrediente secreto aquí es preservar las cabeceras Host originales para que la aplicación backend (en este caso, Laravel) genere las URLs autorreferenciales correctamente.

Aquí está la lógica central en function/[[route]].ts:

import { Hono, MiddlewareHandler as _MiddlewareHandler } from "hono";
import { cache } from "hono/cache";
import { handle, serveStatic } from "hono/cloudflare-pages";
import { cors } from "hono/cors";
import { HTTPException } from "hono/http-exception";
import { logger } from "hono/logger";

type Bindings = {
  LAMBDA_FUNCTION_URL: string;
  DB: D1Database;
};

type MiddlewareHandler = _MiddlewareHandler<{ Bindings: Bindings }>;

function createProxiedUrl(requestUrl: URL, targetUrl: URL) {
  const url = new URL(requestUrl);
  url.host = targetUrl.host;
  url.protocol = targetUrl.protocol;
  url.port = targetUrl.port;
  return url;
}

const handleProxiedRequest: MiddlewareHandler = async (c) => {
  const { LAMBDA_FUNCTION_URL } = c.env;

  if (!LAMBDA_FUNCTION_URL) {
    throw new HTTPException(502, { message: "Internal Server Error" });
  }

  const requestUrl = new URL(c.req.url);
  const proxiedUrl = createProxiedUrl(requestUrl, new URL(LAMBDA_FUNCTION_URL));

  // Crear la petición que será enviada a AWS Lambda
  const newRequest = new Request(proxiedUrl.toString(), c.req.raw);

  // Crítico: Reenviar el Host original para que el backend conozca el dominio real
  newRequest.headers.set("Host", requestUrl.hostname);
  newRequest.headers.set("X-Forwarded-Host", requestUrl.hostname);

  // Pasar Metadata de Cloudflare (Geo, etc.) a Lambda vía Headers
  const cloudflareMetadata = c.req.raw.cf || {};

  for (const key in cloudflareMetadata) {
    const value = cloudflareMetadata[key];
    const headerKey = `X-CF-${key}`;

    if (typeof value === "string") {
      newRequest.headers.set(headerKey, value);
    } else if (typeof value !== "undefined" && typeof value === "object") {
      newRequest.headers.set(headerKey, JSON.stringify(value));
    }
  }

  // Enviar la petición a AWS Lambda y obtener la respuesta
  const response = await fetch(newRequest);

  // Crear una nueva respuesta para modificar headers si es necesario (ej. eliminar X-Powered-By)
  const newResponse = new Response(response.body, response);
  newResponse.headers.delete("X-Powered-By");

  return newResponse;
};

// ... Código adicional de manejo estático abajo
export const onRequest = handle(app);

Me encanta esta configuración por la facilidad con la que manejamos las propiedades del objeto cf. Al iterar sobre c.req.raw.cf, inyectamos datos valiosos de geolocalización y de la petición en cabeceras prefijadas con X-CF-. Esto le da a nuestra función Lambda contexto sobre el usuario (País, Ciudad, Latitud) sin requerir integraciones complejas de AWS.

2. Manejo de Archivos (Ficheros) Estáticos

AWS Lambda es notoriamente complicado al devolver archivos (ficheros) binarios como imágenes. A menudo requiere codificación en base64, lo que aumenta el tamaño del payload y la latencia. Para resolver esto, servimos los activos estáticos directamente desde el edge usando la lógica de Cloudflare Pages.

Configuramos el worker para interceptar rutas como /build/*, /css/* y /js/* y servirlas desde la caché de Cloudflare, evitando la función Lambda por completo.

const handleStatic: MiddlewareHandler = async (c, next) => {
  const response = await serveStatic()(c, next);
  if (!response || response?.status === 404) {
    return c.notFound();
  }
  return new Response(response.body, response);
};

// Caché agresiva para los assets de build
app.all(
  "/build/*",
  cache({
    cacheName: "build-static",
    cacheControl: "public, max-age=31536000, immutable",
  }),
  handleStatic,
);

app.all("/*", handleProxiedRequest);

Para maximizar los beneficios de la CDN de Cloudflare, también configuramos los archivos (ficheros) _headers y _routes.json. Este paso es crucial para definir cabeceras de seguridad que protejan la aplicación.

Archivo (Fichero) _headers

/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: SAMEORIGIN
  X-UA-Compatible: ie=edge
  X-XSS-Protection: 1; mode=block
  Feature-Policy: fullscreen 'self'; camera 'none'; geolocation 'none'; microphone 'none'
  Referrer-Policy: same-origin

/favicon.ico
  Cache-Control: public, max-age=86400

/robots.txt
  Cache-Control: public, max-age=86400

/sw.js
  Cache-Control: no-cache

Archivo (Fichero) _routes.json

{
  "version": 1,
  "include": ["/*"],
  "exclude": ["/favicon.ico", "/robots.txt", "/sw.js"]
}

3. Despliegue con GitHub Actions

La automatización no es opcional; es supervivencia. Los despliegues manuales son una receta para el desastre. Establecimos un pipeline de CI/CD usando GitHub Actions para desplegar el backend Laravel/PHP en AWS Lambda (vía Bref) y el proxy frontend en Cloudflare simultáneamente.

El flujo de trabajo es crítico para mantener los dos entornos sincronizados:

  1. Construir Frontend: Compilar los assets.
  2. Desplegar Backend: Hacer push de la aplicación PHP a AWS Lambda usando Serverless Framework/Bref.
  3. Desplegar Proxy: Hacer push del código del worker y los assets estáticos a Cloudflare Pages.

Aquí está la configuración de deploy.yml:

name: Deploy

on: workflow_dispatch

jobs:
  deploy-dev:
    name: AWS Lambda/Cloudflare workers
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - uses: actions/setup-node@v4
        with:
          node-version: "18"
          cache: "npm"

      - name: Install node modules
        run: npm install

      - name: Build
        run: npm run build

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"

      - name: Install Composer dependencies
        uses: "ramsey/composer-install@v2"
        with:
          composer-options: "--prefer-dist --optimize-autoloader --no-dev"

      - name: Build
        # "php artisan config:cache" no es necesario.
        # Bref lo hará por nosotros.
        run: |
          php artisan event:cache
          php artisan route:cache
          php artisan view:cache
          php artisan icons:cache

      - name: Deploy to AWS Lambda
        uses: serverless/github-action@v3
        with:
          args: deploy --stage=dev
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Run migrations
        run: npx serverless bref:cli --args="migrate --force"
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: project-name
          branch: main
          directory: ./public
          wranglerVersion: "3"

Los Resultados

El impacto financiero fue inmediato. Al cambiar la capa de enrutamiento a Cloudflare, el costo (coste) cayó dramáticamente para nuestro escenario de alta escala.

Escenario A: 1.000 Millones de Peticiones

Cuando hacemos los números para un escenario empresarial de alto tráfico, la diferencia es innegable.

ServicioCosto Mensual (USD)
AWS HTTP API$930.00
AWS REST API$3,033.10
Cloudflare Workers$192.85

Hacer proxy vía Cloudflare Workers añade aproximadamente 30ms de latencia en promedio—un compromiso (trade-off) insignificante por un costo (coste) total de $192.85. Eso es un 80% más barato que API Gateway HTTP API y un 94% más barato que API Gateway REST API.

Escenario B: 1 Millón de Peticiones (El Trade-off)

Sin embargo, debemos ser transparentes sobre cuándo no usar esto. Si estás operando a una escala más pequeña, AWS podría ser en realidad más barato y simple.

ServicioCosto Mensual (USD)
AWS HTTP API~$1.00
AWS REST API~$3.50
Cloudflare Workers~$5.00

En volúmenes bajos, el mínimo de $5 para el plan de pago de Cloudflare Workers excede los centavos que pagarías a AWS. Esta solución es estrictamente una optimización para la escala. Si tu aplicación tiene poco tráfico, quédate con API Gateway.

Lecciones Aprendidas y Trade-offs

Nunca dejo de buscar mejores formas de cumplir mi misión, pero “mejor” siempre viene con compromisos (trade-offs). Aquí está lo que necesitas considerar antes de migrar:

  • Complejidad de Desarrollo:

  • Desarrollo Local: Esta arquitectura complica el desarrollo local. Efectivamente necesitas dos procesos corriendo: uno para tu backend y un segundo proceso Node.js para emular el proxy de Cloudflare Worker.

  • Coordinación CI/CD: Debes asegurarte de que la LAMBDA_FUNCTION_URL se pase correctamente al Cloudflare Worker durante el despliegue, requiriendo una gestión cuidadosa de las variables de entorno.

  • Tarifas de Egreso (Egress Fees): Aunque Cloudflare Workers (no enterprise) generalmente no cobra por egreso, AWS cobra por los datos que salen (data out) desde Lambda hacia internet (lo cual incluye la respuesta de vuelta a Cloudflare). Esta factura existirá independientemente de tu elección de gateway.

  • Arranques en Frío (Cold Starts): Estás encadenando dos tecnologías serverless. Aunque Cloudflare Workers tiene un arranque en frío casi nulo, tu AWS Lambda todavía necesita despertar. El salto añadido puede exacerbar ligeramente la sensación de “arranque en frío” para el primer usuario.

  • Balanceo de Carga: Para escenarios de escala extremadamente alta, Laravel Vapor y Bref recomiendan usar un AWS Application Load Balancer (ALB). Aunque robustos, los ALBs tienen un costo (coste) mínimo que puede ser más alto que el enfoque de Workers para tráfico esporádico.

Pensamientos Finales

Esta arquitectura me permitió descubrir alternativas para escalar eficientemente sin el miedo a una factura de AWS fuera de control. Pone el control de nuevo en mis manos, permitiendo la innovación con calidad y armonía.

Si te enfrentas a problemas de escala similares, te animo a probar este enfoque. Reemplazar un servicio central de AWS puede parecer desalentador, pero los ahorros y la flexibilidad valen la pena el esfuerzo.

¿Has intentado reemplazar API Gateway? Deja tu comentario abajo o comparte tus propias victorias de optimización de costos (costes).