Llevo como 3 años usando ambas y cada pocos meses me encuentro el mismo debate en revisiones de arquitectura. Alguien sugiere Lambda@Edge para hacer una cosa, yo propongo CloudFront Functions, y pasamos 20 minutos explicando las diferencias. Este artículo es la versión corta de esa conversación con ejemplos reales de cuándo elegí una y cuándo la otra.
La tesis es simple. CloudFront Functions es más rápido, más barato, y más limitado. Lambda@Edge es más potente pero tiene overhead de latencia y complejidad operacional. La mayoría de casos que la gente resuelve con Lambda@Edge son mejor resueltos con CloudFront Functions, pero no todos.
Lo que las hace distintas
flowchart LR
A[Request] --> B{Tipo de trabajo}
B -->|Manipular headers, redirects, URL rewrites| C[CloudFront Functions]
B -->|Llamar APIs, DynamoDB, lectura compleja| D[Lambda@Edge]
C --> E[JavaScript ES5.1]
C --> F[1ms max]
C --> G[Sin fetch, sin imports]
C --> H[$0.10 por millón]
D --> I[Node.js o Python moderno]
D --> J[Hasta 30s en origin events]
D --> K[fetch, SDK completo]
D --> L[$0.60 por millón + compute]
CloudFront Functions corren en los puntos de presencia (POPs) en un runtime JavaScript súper limitado, con un timeout duro de 1 millisegundo de CPU. No puedes hacer fetch, no puedes importar librerías, no hay async. Solo manipulas el objeto request o response.
Lambda@Edge corre en regional edge caches (no en POPs, están más adentro), con Node.js o Python completo. Puedes hacer HTTP requests, leer DynamoDB, importar AWS SDK, todo.
Esta diferencia arquitectónica explica todo lo demás. CloudFront Functions es esencialmente un middleware en memoria que ejecuta en nanosegundos. Lambda@Edge es una Lambda que vive cerca del usuario, con todas las capacidades y también todos los costos de una Lambda normal.
Comparativa por métricas reales
Estas son mediciones de proyectos en producción, no números de marketing.
| Métrica | CloudFront Functions | Lambda@Edge |
|---|---|---|
| Cold start | Nunca (corre en POPs) | 20-100ms primera invocación |
| Warm invocation | Sub-millisegundo | 1-5ms overhead |
| Costo por millón | $0.10 | $0.60 + $0.00005 por GB-s |
| Memoria | 2MB de config + código | 128MB a 10GB |
| Runtime | JavaScript ES5.1 estricto | Node.js 18/20, Python 3.9-3.11 |
| Request body | No accesible | Accesible (con limits) |
| Response body | No accesible | Accesible (origin response) |
| Network calls | No | Sí (con overhead) |
| Cuánto puedes debuggear | Console logs básicos | CloudWatch completo, X-Ray |
El número que más le importa a la gente es el costo. Una app que mueve 500M de requests al mes paga $50 en CloudFront Functions vs $300 base en Lambda@Edge (y sube rápido con memoria y duración). Para rewrites triviales, la diferencia es obvia.
Cuándo CloudFront Functions gana claro
1. URL rewrites y redirects simples.
Este es el 80% de mis casos. Remover slash final, normalizar mayúsculas, redirigir /old-path a /new-path.
// CloudFront Function: viewer-request
function handler(event) {
var request = event.request;
var uri = request.uri;
// Remover trailing slash excepto en root
if (uri.length > 1 && uri.endsWith('/')) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
location: { value: uri.slice(0, -1) }
}
};
}
// Agregar index.html para SPA paths
if (!uri.includes('.') && !uri.endsWith('/')) {
request.uri = uri + '/index.html';
}
return request;
}
Esto ejecuta en 0.03ms promedio. La misma lógica en Lambda@Edge agregaría al menos 3-4ms y 6x el costo.
2. Headers de seguridad estáticos.
Si los headers son los mismos para toda la app, CloudFront Functions los inyecta sin pensar.
function handler(event) {
var response = event.response;
var headers = response.headers;
headers['strict-transport-security'] = {
value: 'max-age=63072000; includeSubDomains; preload'
};
headers['x-content-type-options'] = { value: 'nosniff' };
headers['x-frame-options'] = { value: 'DENY' };
headers['referrer-policy'] = { value: 'strict-origin-when-cross-origin' };
headers['permissions-policy'] = {
value: 'camera=(), microphone=(), geolocation=()'
};
return response;
}
Para headers dinámicos (CSP con nonces, por ejemplo), necesitas Lambda@Edge porque requieres generar valores únicos por request. Eso es otro caso.
3. Normalización de cache keys.
Cuando tu app tiene muchas variaciones de query strings que apuntan al mismo contenido, CloudFront Functions normaliza antes de que el cache decida hit o miss.
function handler(event) {
var request = event.request;
var qs = request.querystring;
// Remover utm_ params que no afectan contenido
var cleanedQs = {};
for (var key in qs) {
if (!key.startsWith('utm_') && key !== 'fbclid' && key !== 'gclid') {
cleanedQs[key] = qs[key];
}
}
request.querystring = cleanedQs;
return request;
}
Una app con tráfico de marketing puede duplicar su cache hit rate con esto. Lo he visto pasar de 62% a 89% en un cliente.
4. Auth básica con JWT ya firmado.
Si el JWT viene en cookie o header y solo necesitas validar firma y expiración, CloudFront Functions lo hace sin red.
import crypto from 'crypto';
function handler(event) {
var request = event.request;
var token = request.cookies.session && request.cookies.session.value;
if (!token) {
return {
statusCode: 302,
headers: { location: { value: '/login' } }
};
}
// Validar firma JWT (simplificado, sin crypto real)
var parts = token.split('.');
if (parts.length !== 3) {
return { statusCode: 401, body: 'Invalid token' };
}
var payload = JSON.parse(
String.bytesFrom(parts[1], 'base64').toString('utf-8')
);
if (payload.exp * 1000 < Date.now()) {
return {
statusCode: 302,
headers: { location: { value: '/login?expired=1' } }
};
}
return request;
}
Nota: la crypto de CloudFront Functions tiene sus limitaciones. Para validar HMAC con una clave secreta, funciona. Para verificar RSA con una llave pública JWKS, necesitas Lambda@Edge porque requieres fetch.
Cuándo Lambda@Edge es la respuesta correcta
1. Decisiones que dependen de llamadas externas.
Si necesitas consultar DynamoDB, un API, o cualquier servicio fuera del edge, CloudFront Functions no sirve. No tiene fetch ni SDK.
// lambda-edge/geo-pricing.js
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { GetItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({ region: "us-east-1" });
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const country =
headers["cloudfront-viewer-country"]?.[0]?.value || "US";
const result = await client.send(
new GetItemCommand({
TableName: "CountryPricing",
Key: { country: { S: country } },
})
);
const multiplier = result.Item?.multiplier?.N || "1";
request.headers["x-price-multiplier"] = [
{ key: "X-Price-Multiplier", value: multiplier }
];
return request;
};
Este patrón lo uso en un ecommerce que ajusta precios por país en el edge. CloudFront Functions no puede leer de DynamoDB, punto.
2. Manipulación de response body.
CloudFront Functions no puede tocar el body de la respuesta. Lambda@Edge en el event origin-response sí puede.
// lambda-edge/inject-meta-tags.js
export const handler = async (event) => {
const response = event.Records[0].cf.response;
const request = event.Records[0].cf.request;
if (
response.headers["content-type"]?.[0]?.value.includes("text/html")
) {
let body = response.body;
// Inyectar meta tags dinámicas basadas en el path
const path = request.uri;
const metaTags = generateMetaTags(path);
body = body.replace("</head>", `${metaTags}</head>`);
response.body = body;
response.bodyEncoding = "text";
}
return response;
};
function generateMetaTags(path) {
// lógica para generar meta tags específicas del path
return `<meta property="og:url" content="https://mysite.com${path}">`;
}
Este caso llega cuando tienes SPA con contenido dinámico y quieres meta tags OG correctos para crawlers sin migrar a SSR completo.
3. CSP con nonces dinámicos.
Cada request necesita un nonce único que se inyecta en el HTML y en el header CSP. No puedes hacer esto con JavaScript ES5.1 sin crypto decente, y el nonce tiene que viajar entre el header response y el body response.
// lambda-edge/csp-nonce.js
import crypto from "crypto";
export const handler = async (event) => {
const response = event.Records[0].cf.response;
const nonce = crypto.randomBytes(16).toString("base64");
response.headers["content-security-policy"] = [
{
key: "Content-Security-Policy",
value:
`default-src 'self'; ` +
`script-src 'self' 'nonce-${nonce}'; ` +
`style-src 'self' 'nonce-${nonce}'`
}
];
if (response.body && response.headers["content-type"]?.[0]?.value.includes("html")) {
response.body = response.body.replace(/<script /g, `<script nonce="${nonce}" `);
response.body = response.body.replace(/<style /g, `<style nonce="${nonce}" `);
response.bodyEncoding = "text";
}
return response;
};
4. Autenticación con validación de firma pública.
Cuando necesitas verificar JWT firmado por Cognito o Auth0, necesitas JWKS públicas y eso requiere fetch.
// lambda-edge/verify-cognito-jwt.js
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxx/.well-known/jwks.json")
);
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const authHeader = request.headers.authorization?.[0]?.value;
if (!authHeader) {
return { status: "401", body: "Missing token" };
}
try {
const token = authHeader.replace("Bearer ", "");
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxx",
audience: "my-client-id"
});
request.headers["x-user-id"] = [
{ key: "X-User-Id", value: payload.sub }
];
return request;
} catch (e) {
return { status: "401", body: "Invalid token" };
}
};
Árbol de decisión práctico
flowchart TD
A[Necesito lógica en edge] --> B{¿Necesita llamar APIs externas?}
B -->|Sí| L[Lambda@Edge]
B -->|No| C{¿Necesita leer o modificar response body?}
C -->|Sí| L
C -->|No| D{¿Solo toca headers, URI o status?}
D -->|Sí| E{¿La lógica cabe en 1ms CPU?}
E -->|Sí| CF[CloudFront Functions]
E -->|No| L
D -->|No| L
En la práctica, este árbol resuelve el 95% de los casos. Solo cuando tengo duda real, hago benchmarks.
Un caso donde me equivoqué
Hace dos años implementé un sistema de A/B testing en Lambda@Edge. Cada request pasaba por una Lambda que leía DynamoDB para saber qué variante asignar al usuario basado en cookie. Funcionaba, pero era caro: $800 USD al mes solo por ese Lambda en tráfico medio.
Me di cuenta que la asignación de variante en realidad solo necesitaba:
- Revisar si la cookie
variantexistía - Si no, asignar 50/50 y setear cookie
- Inyectar header
x-variant
Eso no necesita DynamoDB. La "asignación" es un hash determinístico del user ID o un random simple. Migré a CloudFront Functions:
function handler(event) {
var request = event.request;
var variant = request.cookies.variant && request.cookies.variant.value;
if (!variant) {
variant = Math.random() < 0.5 ? 'A' : 'B';
if (!request.headers['cookie']) {
request.headers['cookie'] = { value: '' };
}
request.headers['cookie'].value +=
`; variant=${variant}; path=/; max-age=2592000`;
}
request.headers['x-variant'] = { value: variant };
return request;
}
Costo: $50/mes. El tracking de qué variante vio el usuario lo hace el analytics en el cliente, no necesito persistir eso en DynamoDB desde el edge. Me ahorré $750 al mes por cambiar la arquitectura, no la plataforma.
Lo que aprendí en producción
1. El 1ms de CPU no es 1ms de tiempo real.
Es 1ms de CPU time, lo cual es mucho en operaciones sencillas. Un loop que toque 10,000 headers se puede pasar. Un parse de JWT con validación HMAC también. Mide con console.time antes de subir.
2. CloudFront Functions no tiene dependencias.
No hay npm, no hay imports. Todo el código va en un solo archivo y solo tienes las APIs built-in del runtime. Si necesitas una librería, reescríbela inline o pasate a Lambda@Edge.
3. Lambda@Edge se replica a todas las regiones.
Cuando deployas, AWS replica tu Lambda a todas las edge locations. Eso toma 5-10 minutos. Durante ese tiempo, distintos usuarios ven distintas versiones. Planea deploys con cuidado.
4. Los logs de CloudFront Functions son limitados.
Solo llegan a CloudWatch si configuras logging explícito, y los logs cuestan. Para debugging, usa test events en la consola de CloudFront Functions, no producción.
5. Ambos tienen eventos diferentes disponibles.
CloudFront Functions solo corre en viewer-request y viewer-response. Lambda@Edge corre en esos más origin-request y origin-response. Si necesitas tocar algo entre CloudFront y el origin, Lambda@Edge es la única opción.
6. El vendor lock-in es real.
Código de CloudFront Functions es específico de AWS. No se parece a Cloudflare Workers ni Vercel Edge Functions. Migrar entre clouds implica reescribir. Tenlo en mente si hay probabilidad de cambiar proveedor.
Cuándo no uses ni uno ni el otro
Si tu lógica solo afecta a una pequeña parte del tráfico (digamos rutas bajo /admin), considera resolverla en tu origin (Lambda SSR o backend) en lugar del edge. Agregar otra capa de edge function para algo que no se ejecuta frecuentemente no compensa la complejidad.
Si estás empezando un proyecto y no sabes si lo vas a escalar, no pongas lógica en edge por principio. CloudFront con políticas de cache estáticas y un backend normal te llevan lejos. Edge functions son optimización, no requisito.
Si tu equipo no conoce edge computing, la curva de aprendizaje vs beneficio no vale la pena. Lo que ahorras en latencia lo pierdes en horas de debugging. Primero domina CloudFront básico, luego edge functions.
En el próximo artículo miro DynamoDB Single-Table Design desde el lado del frontend. Cómo modelar acceso pensado en lo que el componente necesita, no en lo que el backend prefiere. Con ejemplos de un dashboard real en Next.js.
Top comments (0)