Next.js was historically designed by Vercel with tight coupling to its proprietary serverless platform. When executing next build, compilation artifacts are emitted into the .next directory, which in its monolithic form is structured either for a long-lived Node.js process (node server.js) or for proprietary Vercel deployment primitives.
Attempting to run a monolithic node server.js binary directly within a single AWS Lambda function introduces severe architectural bottlenecks:
/_next/static/* wakes a heavyweight Lambda micro-VM, consuming concurrency limits and inflating cloud compute bills.next/image with the native sharp library saturates 100% of a Lambda container's single-core CPU, delaying concurrent dynamic SSR requests./tmp filesystem in Lambda causes Incremental Static Regeneration (ISR) data loss and eliminates on-demand tag revalidation (revalidateTag) across horizontally scaled instances.To solve this challenge, the open-source community created OpenNext. OpenNext dissects the Next.js 15 build, mapping its subsystems to optimized native AWS services: AWS Lambda (Server, Image, Revalidation), Amazon S3, Amazon CloudFront, Amazon SQS, and Amazon DynamoDB.
The OpenNext deployment topology routes and isolates traffic at the CloudFront Content Delivery Network layer:
Incoming Client Request (HTTPS)
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Amazon CloudFront CDN (Global Anycast) │
│ - TLS 1.3 Termination, HTTP/3, Brotli/Gzip Compression │
│ - URI Path-Based Cache Behaviors Routing │
└───────┬──────────────────────────┬─────────────────────────────┬───────┘
│ /_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) and image optimization to CloudFront + S3 cuts bandwidth and compute costs by 4x–7x compared to commercial developer platforms.Executing npx open-next build inspects the Next.js compiler output (.next/standalone, .next/static, .next/server) and generates a dedicated .open-next/ bundle:
Generates standalone server entrypoint .next/standalone, client bundles in .next/static, and server page manifests.
Separates route handlers from image optimization routines and compiles AWS Lambda event wrappers with response streaming.
Uploads client JavaScript, CSS, and public media to an S3 bucket with immutable cache-control headers.
Maps request URI patterns to corresponding origins: S3 for static assets, Lambda Function URLs with Response Streaming for SSR.
Next.js 15 enables external cache governance via the cacheHandler property in next.config.ts. Below is the implementation of an AWS distributed cache adapter persisting metadata to DynamoDB and payloads to 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;
}
// Retrieve cached entry by key
async get(key) {
try {
// 1. Verify metadata, tags, and expiration in DynamoDB
const meta = await ddb.send(
new GetCommand({
TableName: DDB_TABLE,
Key: { cacheKey: key },
})
);
if (!meta.Item) return null;
// Verify absolute Time-to-Live (TTL) expiration
if (meta.Item.revalidateAt && Date.now() > meta.Item.revalidateAt) {
return null;
}
// 2. Fetch binary cache payload (HTML / RSC JSON) from 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;
}
}
// Persist newly rendered payload to distributed storage
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. Persist cache payload into S3
await s3.send(
new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: `cache/${key}`,
Body: JSON.stringify(data),
ContentType: 'application/json',
})
);
// 2. Register cache tags and metadata in 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);
}
}
// Invalidate cache entries associated with a tag
async revalidateTag(tag) {
console.log(`[AwsCacheHandler] Invalidating cache tag: ${tag}`);
}
}The following manifest defines an enterprise Next.js 15 deployment on AWS using the SST v3 (Ion) engine. It provisions CloudFront, an S3 asset bucket with OAC, a DynamoDB table for cache tags, ARM64 Graviton3 compute, and IAM policies.
/// <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. Amazon S3 Bucket for persistent ISR and Data Cache storage
const cacheBucket = new sst.aws.Bucket("NextJsSharedCache", {
access: "private",
});
// 2. Amazon DynamoDB Table for cache tag registries (revalidateTag)
const cacheTagTable = new sst.aws.Dynamo("NextJsCacheTagRegistry", {
fields: {
cacheKey: "string",
tag: "string",
},
primaryIndex: { hashKey: "cacheKey" },
globalIndexes: {
TagIndex: { hashKey: "tag" },
},
ttl: "expireAt",
});
// 3. Deploy Next.js application using OpenNext engine
const site = new sst.aws.Nextjs("EnterpriseWeb", {
// ARM64 (AWS Graviton3) provides ~25% lower cost and higher performance
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!,
},
// Grant IAM permissions for reading and writing to cache backends
link: [cacheBucket, cacheTagTable],
// Global CloudFront CDN configuration
cdn: {
transform: {
distribution(args) {
// Enable HTTP/3 to minimize latency on mobile connections
args.httpVersion = "http2and3";
// Enforce modern TLS protocols for security compliance
args.viewerCertificate = {
minimumProtocolVersion: "TLSv1.2_2021",
sslSupportMethod: "sni-only",
};
},
},
},
// Isolate image optimization in an independent Lambda function
imageOptimization: {
memory: "1536 MB",
staticImageOptimization: true,
},
});
return {
siteUrl: site.url,
distributionId: site.nodes.cdn?.id,
cacheBucketName: cacheBucket.name,
};
},
});Deploying OpenNext behind a traditional AWS API Gateway REST/HTTP API. The browser expects progressive streaming of HTML and RSC chunks (loading.tsx), but the page hangs blank and renders all at once after 3–5 seconds.
Standard API Gateway buffers the entire Lambda response in memory and releases it only after the function completes execution. Real-time streaming requires AWS Lambda Function URLs configured with InvokeMode: RESPONSE_STREAM, which OpenNext provisions automatically.
// ❌ ANTI-PATTERN: Routing through standard API Gateway without streaming support
// Buffers entire payload in memory, breaking Suspense boundaries and TTFB
// ✅ ARCHITECTURAL FIX: Lambda Function URL with Native Response Streaming
// OpenNext activates the streaming response wrapper:
// awslambda.streamifyResponse(async (event, responseStream, context) => { ... })During a traffic spike to 1,000 requests per second, AWS Lambda scales horizontally to 1,000 concurrent micro-VMs. If each Serverless Function establishes its own database connection pool (max: 10), the PostgreSQL server is hit with 10,000 simultaneous TCP sockets, crashing with FATAL: remaining connection slots are reserved for non-replication superuser connections.
// ❌ CATASTROPHIC ERROR: Unpooled direct Postgres connection inside Serverless Lambda
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
// ✅ PRODUCTION FIX: Route traffic through AWS RDS Proxy or PgBouncer
// RDS Proxy maintains a multiplexed connection pool to PostgreSQL,
// sharing a small number of database connections across thousands of transient Lambdas.Configuring CloudFront caching on the root path /* without including Authorization and Cookie in the Cache Key headers list. The first authenticated user opening their account dashboard causes their private data to be cached at the CDN edge, exposing personal details to subsequent anonymous visitors.
// ❌ CRITICAL SECURITY VULNERABILITY: Caching dynamic paths without session isolation
// CloudFront Default Behavior: MinTTL = 60, Forward Cookies = None
// ✅ PRODUCTION FIX: Explicit Cache Behavior Separation
// Static (/_next/static/*): Cache for 1 year, ignore all cookies and query strings.
// Dynamic (/*): CachePolicy = CachingDisabled, OriginRequestPolicy = AllViewerExceptHostHeader.Building Lambda functions under the default x86_64 architecture. AWS Graviton3 processors (arm64) execute V8 JavaScript instructions up to 20% faster while reducing compute costs by 20% per millisecond. Neglecting to set architecture: "arm64" results in avoidable infrastructure overhead.
Ensure native binary dependencies (e.g., sharp) are compatible with Linux ARM64 and declare the custom cacheHandler in next.config.ts.
Provision a private S3 bucket secured with OAC for static assets, DynamoDB for cache tag registries, and an SQS queue for background ISR revalidation.
Assign execution roles granting S3 and DynamoDB access, and configure Lambda Function URLs with Response Streaming mode.
Run open-next build, synchronize static files to S3 with immutable cache headers, and upload Server and Image Lambda packages.
Verify that Authorization and Cookie headers bypass cache on dynamic paths, test TTFB on streaming Suspense routes, and confirm static asset 200 HIT responses.