Históricamente, Next.js fue concebido por Vercel con una profunda integración en su propia infraestructura serverless propietaria. Al compilar un proyecto (next build), los artefactos resultantes se generan en el directorio .next, el cual, en su formato monolítico, está concebido para ejecutarse en un proceso de Node.js permanente (node server.js) o para traducirse de forma opaca hacia los recursos internos de Vercel.
Intentar ejecutar un binario monolítico node server.js dentro de una única función AWS Lambda genera graves cuellos de botella arquitectónicos:
/_next/static/* despierta un microcontenedor Lambda pesado, agotando cuotas de concurrencia y encareciendo sustancialmente los costes de computación.next/image mediante la librería nativa sharp acapara el 100% de la CPU de una instancia Lambda, retrasando el procesamiento de peticiones dinámicas concurrentes./tmp en Lambda provoca la pérdida de datos de Incremental Static Regeneration (ISR) e impide el funcionamiento de la invalidación por etiquetas (revalidateTag) entre múltiples réplicas horizontales.Para solucionar este desafío, la comunidad de código abierto desarrolló OpenNext. OpenNext realiza ingeniería inversa sobre el compilado de Next.js 15, transformando el monolito en servicios especializados de AWS: AWS Lambda (Server, Image, Revalidation), Amazon S3, Amazon CloudFront, Amazon SQS y Amazon DynamoDB.
La topología de despliegue de Next.js 15 con OpenNext aísla y canaliza el tráfico a través de la red perimetral de Amazon CloudFront:
Petición cliente entrante (HTTPS)
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Amazon CloudFront CDN (Global Anycast) │
│ - Terminación TLS 1.3, HTTP/3, Compresión Brotli/Gzip │
│ - Enrutamiento por Cache Behaviors según el patrón de URI │
└───────┬──────────────────────────┬─────────────────────────────┬───────┘
│ /_next/static/* │ /_next/image* │ Default /*
▼ ▼ ▼
┌──────────────────┐ ┌─────────────────────┐ ┌───────────────────┐
│ Amazon S3 │ │ Image Lambda │ │ Server Lambda │
│ (Static Assets) │ │ (sharp, ARM64) │ │ (SSR, Actions) │
│ OAC Restricted │ │ WebP / AVIF Resize │ │ Response Stream │
└──────────────────┘ └──────────┬──────────┘ └─────────┬─────────┘
│ │
▼ │ SWR ISR
┌─────────────────────┐ ▼
│ S3 Assets Storage │ ┌───────────────────┐
│ (Optimized Images) │ │ Amazon SQS │
└─────────────────────┘ │ (ISR Queue) │
│
▼
┌───────────────────┐
│ Revalidation │
│ Lambda Worker │
└─────────┬─────────┘
│
┌─────────────────────────┴─────────┐
▼ ▼
┌─────────────────────┐ ┌───────────────────┐
│ Amazon DynamoDB │ │ Amazon S3 │
│ (Cache Tag Registry)│ │ (ISR HTML/RSC) │
└─────────────────────┘ └───────────────────┘revalidateTag)..next/static) y procesamiento gráfico en la combinación CloudFront + S3 reduce los costes de transferencia y computación entre 4 y 7 veces frente a PaaS comerciales.Al ejecutar npx open-next build, la herramienta examina la salida del compilador de Next.js (.next/standalone, .next/static, .next/server) y genera el paquete optimizado .open-next/:
Generación del servidor autónomo .next/standalone, recursos estáticos .next/static y manifiestos de páginas de servidor.
Separación de manejadores de rutas dinámicas de la optimización de imágenes y compilación de envoltorios Lambda con streaming.
Transferencia de JavaScript cliente, CSS y archivos multimedia a un bucket S3 con directivas de caché inmutables.
Asociación de patrones de URI con orígenes: S3 para contenido estático y Lambda Function URLs con streaming para SSR.
Next.js 15 permite delegar la gestión de la caché mediante la opción cacheHandler en next.config.ts. A continuación se muestra la implementación de un proveedor distribuido que persiste metadatos en DynamoDB y contenidos pesados en Amazon S3:
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb';
const s3 = new S3Client({ region: process.env.AWS_REGION });
const ddbRaw = new DynamoDBClient({ region: process.env.AWS_REGION });
const ddb = DynamoDBDocumentClient.from(ddbRaw);
const BUCKET_NAME = process.env.NEXT_CACHE_BUCKET_NAME;
const DDB_TABLE = process.env.NEXT_CACHE_DYNAMO_TABLE;
export default class AwsDistributedCacheHandler {
constructor(options) {
this.options = options;
}
// Recuperación de un elemento de caché mediante su clave
async get(key) {
try {
// 1. Comprobar metadatos, etiquetas y tiempo de vida en DynamoDB
const meta = await ddb.send(
new GetCommand({
TableName: DDB_TABLE,
Key: { cacheKey: key },
})
);
if (!meta.Item) return null;
// Verificar expiración absoluta del tiempo de vida (TTL)
if (meta.Item.revalidateAt && Date.now() > meta.Item.revalidateAt) {
return null;
}
// 2. Descargar el contenido compilado (HTML / JSON de RSC) desde Amazon S3
const s3Response = await s3.send(
new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: `cache/${key}`,
})
);
const bodyString = await s3Response.Body.transformToString();
const value = JSON.parse(bodyString);
return {
lastModified: meta.Item.lastModified,
value,
};
} catch (error) {
console.error(`[AwsCacheHandler] Cache read error for key: ${key}`, error);
return null;
}
}
// Guardar nuevos artefactos en el almacenamiento distribuido
async set(key, data, ctx) {
try {
const now = Date.now();
const revalidate = typeof ctx.revalidate === 'number' ? ctx.revalidate : false;
const revalidateAt = revalidate ? now + revalidate * 1000 : null;
// 1. Guardar la carga útil en S3
await s3.send(
new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: `cache/${key}`,
Body: JSON.stringify(data),
ContentType: 'application/json',
})
);
// 2. Registrar etiquetas y metadatos en DynamoDB
await ddb.send(
new PutCommand({
TableName: DDB_TABLE,
Item: {
cacheKey: key,
tags: ctx.tags || [],
lastModified: now,
revalidateAt,
},
})
);
} catch (error) {
console.error(`[AwsCacheHandler] Cache write error for key: ${key}`, error);
}
}
// Invalidación de entradas vinculadas a una etiqueta
async revalidateTag(tag) {
console.log(`[AwsCacheHandler] Invalidating cache tag: ${tag}`);
}
}El siguiente manifiesto ilustra una configuración de producción con el motor SST v3 (Ion). Define automáticamente CloudFront, un bucket S3 protegido con OAC, tablas DynamoDB para etiquetas de ISR, arquitectura ARM64 Graviton3 y roles IAM con privilegios mínimos.
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "enterprise-nextjs-aws",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "aws",
};
},
async run() {
// 1. Bucket de Amazon S3 para almacenamiento persistente de ISR y caché
const cacheBucket = new sst.aws.Bucket("NextJsSharedCache", {
access: "private",
});
// 2. Tabla de Amazon DynamoDB para el registro de etiquetas de caché (revalidateTag)
const cacheTagTable = new sst.aws.Dynamo("NextJsCacheTagRegistry", {
fields: {
cacheKey: "string",
tag: "string",
},
primaryIndex: { hashKey: "cacheKey" },
globalIndexes: {
TagIndex: { hashKey: "tag" },
},
ttl: "expireAt",
});
// 3. Despliegue de Next.js mediante el motor OpenNext
const site = new sst.aws.Nextjs("EnterpriseWeb", {
// La arquitectura ARM64 (AWS Graviton3) ofrece un 25% más de rendimiento a menor coste
architecture: "arm64",
memory: "1536 MB",
timeout: "30 seconds",
openNextVersion: "3.1.0",
environment: {
NEXT_CACHE_BUCKET_NAME: cacheBucket.name,
NEXT_CACHE_DYNAMO_TABLE: cacheTagTable.name,
DATABASE_URL: process.env.DATABASE_URL!,
},
// Permisos IAM para interactuar con la infraestructura de caché
link: [cacheBucket, cacheTagTable],
// Configuración de la red CDN global de CloudFront
cdn: {
transform: {
distribution(args) {
// Habilitar soporte HTTP/3 para minimizar la latencia en dispositivos móviles
args.httpVersion = "http2and3";
// Estándar de cifrado TLS para auditorías de seguridad
args.viewerCertificate = {
minimumProtocolVersion: "TLSv1.2_2021",
sslSupportMethod: "sni-only",
};
},
},
},
// Aislar el procesamiento gráfico en una función Lambda independiente
imageOptimization: {
memory: "1536 MB",
staticImageOptimization: true,
},
});
return {
siteUrl: site.url,
distributionId: site.nodes.cdn?.id,
cacheBucketName: cacheBucket.name,
};
},
});Desplegar OpenNext detrás de un API Gateway tradicional REST/HTTP de AWS. El navegador cliente espera la llegada progresiva de trozos de HTML y RSC (loading.tsx), pero la página queda congelada en blanco y se visualiza completa tras 3 a 5 segundos de espera.
API Gateway retiene en búfer toda la respuesta de la función y la emite únicamente cuando Lambda finaliza su ejecución. Para permitir el streaming interactivo es imprescindible utilizar AWS Lambda Function URLs con la modalidad InvokeMode: RESPONSE_STREAM, aprovisionada de forma nativa por OpenNext.
// ❌ ANTIPATRÓN: Enrutamiento mediante API Gateway tradicional sin soporte de streaming
// Acumula la respuesta en búfer de memoria, destruyendo los límites de Suspense y el TTFB
// ✅ SOLUCIÓN ARQUITECTÓNICA: Lambda Function URL con Response Streaming nativo
// OpenNext activa el envoltorio de transmisión de respuestas:
// awslambda.streamifyResponse(async (event, responseStream, context) => { ... })Ante un aumento súbito de tráfico de 1.000 peticiones por segundo, AWS Lambda escala horizontalmente desplegando 1.000 microcontenedores independientes. Si cada Serverless Function abre su propio pool hacia PostgreSQL (max: 10), la base de datos recibe 10.000 sockets TCP simultáneos, colapsando con el fallo FATAL: remaining connection slots are reserved for non-replication superuser connections.
// ❌ ERROR CRÍTICO: Conexión directa a PostgreSQL sin gestor de pooling en Lambda
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
// ✅ SOLUCIÓN DE PRODUCCIÓN: Enrutar a través de AWS RDS Proxy o PgBouncer
// RDS Proxy retiene un pool persistente multiplexado hacia la base de datos,
// compartiendo un número limitado de sockets entre miles de funciones efímeras.Habilitar la caché de CloudFront sobre la ruta raíz /* sin incluir las cabeceras Authorization y Cookie en la clave de caché (Forward Headers). Como resultado, el primer usuario autenticado que accede a su perfil provoca que sus datos personales se guarden en la CDN, mostrándose a visitantes anónimos posteriores.
// ❌ VULNERABILIDAD GRAVE: Caché de rutas dinámicas sin aislamiento de sesión
// CloudFront Default Behavior: MinTTL = 60, Forward Cookies = None
// ✅ CONFIGURACIÓN SEGURA: Separación estricta de comportamientos
// Estáticos (/_next/static/*): Caché por 1 año, omitir cookies y cabeceras.
// Dinámicos (/*): CachePolicy = CachingDisabled, OriginRequestPolicy = AllViewerExceptHostHeader.Compilar las funciones Lambda bajo la arquitectura estándar x86_64. Los procesadores AWS Graviton3 (arm64) ejecutan el motor V8 de Node.js hasta un 20% más rápido y reducen el coste de computación por milisegundo en un 20%. Omitir el parámetro architecture: "arm64" incrementa los gastos operativos de forma innecesaria.
Verificar la compatibilidad de módulos nativos (ej. sharp) con Linux ARM64 y registrar el manejador custom cacheHandler en next.config.ts.
Crear un bucket S3 privado con OAC para estáticos, tabla DynamoDB para registros de etiquetas y cola SQS para revalidaciones ISR.
Asignar roles de ejecución con permisos sobre S3 y DynamoDB, habilitando Lambda Function URLs con modalidad Response Streaming.
Ejecutar open-next build, sincronizar archivos estáticos .next/static hacia S3 con cabeceras inmutables y actualizar paquetes de Lambda.
Comprobar que cabeceras de autorización omitan la caché dinámica, validar el TTFB del primer fragmento de Suspense y verificar el código 200 HIT en estáticos.