Best Serverless Frameworks for Node.js in 2026
TL;DR
SST for full-stack TypeScript on AWS; Serverless Framework for multi-cloud legacy deployments. SST (~200K weekly downloads) is the modern TypeScript-first framework that deploys to AWS with live function development, type-safe resource binding, and a growing ecosystem. Serverless Framework (~2M downloads) is the older multi-provider tool with thousands of plugins. AWS CDK (~500K) is AWS's official infrastructure-as-code with full TypeScript. For new AWS-native apps in 2026, SST is the compelling choice.
Key Takeaways
- Serverless Framework: ~2M weekly downloads — multi-provider, 1K+ plugins, widely deployed
- SST: ~200K downloads — TypeScript-first, live Lambda dev, resource binding, AWS-native
- AWS CDK: ~500K downloads — low-level AWS infrastructure, TypeScript/Python/Java
- SST v3 — Ion release uses Pulumi under the hood, faster deployments
- Local development — SST live lambda lets you test against real AWS services instantly
Why Serverless
Serverless architecture means deploying functions that scale automatically, with no server management. You pay only when functions run (pay-per-invocation), scale to zero when idle (no charges when no traffic), and don't patch OS or runtime security vulnerabilities.
The economics are compelling for certain workloads: a startup API that handles 100K requests/month costs around $0.20 on Lambda vs $30-50/month for the smallest always-on EC2 instance. For event-driven workloads (webhooks, queue processors, scheduled tasks), serverless is almost always the right economics.
The trade-offs: cold starts (first invocation after idle may take 100-500ms), 15-minute execution limit for Lambda, and vendor lock-in. For latency-sensitive APIs or long-running processes, traditional servers or containers are often better.
Serverless Framework
# serverless.yml — multi-provider configuration
service: my-api
provider:
name: aws
runtime: nodejs20.x
region: us-east-1
stage: ${opt:stage, 'dev'}
environment:
DATABASE_URL: ${ssm:/myapp/${self:provider.stage}/database-url}
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:DeleteItem
Resource: !GetAtt UsersTable.Arn
functions:
createUser:
handler: src/users/create.handler
events:
- httpApi:
path: /users
method: POST
getUser:
handler: src/users/get.handler
events:
- httpApi:
path: /users/{id}
method: GET
processQueue:
handler: src/queue/processor.handler
events:
- sqs:
arn: !GetAtt ProcessingQueue.Arn
batchSize: 10
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${self:provider.stage}-users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
plugins:
- serverless-offline # Local dev
- serverless-esbuild # TypeScript build
- serverless-prune-plugin # Clean old deployments
// src/users/create.ts — Lambda handler
import type { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { z } from 'zod';
const db = new DynamoDBClient({});
const schema = z.object({
name: z.string(),
email: z.string().email(),
});
export const handler: APIGatewayProxyHandler = async (event) => {
try {
const body = schema.parse(JSON.parse(event.body ?? '{}'));
await db.send(new PutItemCommand({
TableName: process.env.USERS_TABLE,
Item: {
id: { S: crypto.randomUUID() },
name: { S: body.name },
email: { S: body.email },
createdAt: { S: new Date().toISOString() },
},
}));
return { statusCode: 201, body: JSON.stringify({ success: true }) };
} catch (err) {
return { statusCode: 400, body: JSON.stringify({ error: String(err) }) };
}
};
SST v3 (Modern AWS)
// sst.config.ts — TypeScript infrastructure
import { SSTConfig } from 'sst';
import { Api, Table, Bucket, NextjsSite, Cron } from 'sst/constructs';
export default {
config(input) {
return {
name: 'my-app',
region: 'us-east-1',
};
},
stacks(app) {
app.stack(function API({ stack }) {
const table = new Table(stack, 'Users', {
fields: { id: 'string', email: 'string' },
primaryIndex: { partitionKey: 'id' },
globalIndexes: {
EmailIndex: { partitionKey: 'email' },
},
});
const bucket = new Bucket(stack, 'Uploads');
const api = new Api(stack, 'Api', {
routes: {
'POST /users': 'packages/functions/src/users/create.handler',
'GET /users/{id}': 'packages/functions/src/users/get.handler',
'POST /upload': 'packages/functions/src/upload.handler',
},
// Type-safe resource binding!
bind: [table, bucket],
});
// Next.js site with SSR
const site = new NextjsSite(stack, 'Web', {
path: 'packages/web',
bind: [api, table],
});
// Cron job
new Cron(stack, 'Cleanup', {
schedule: 'rate(1 day)',
job: { function: 'packages/functions/src/cleanup.handler' },
});
stack.addOutputs({
ApiUrl: api.url,
SiteUrl: site.url,
});
});
},
} satisfies SSTConfig;
// SST resource binding — type-safe, no env vars needed
// packages/functions/src/users/create.ts
import { Table } from 'sst/node/table';
import { Bucket } from 'sst/node/bucket';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
// SST injects the correct table name/bucket name at runtime
// Table.Users.tableName — automatically bound
// Bucket.Uploads.bucketName — automatically bound
const db = new DynamoDBClient({});
export const handler = async (event) => {
await db.send(new PutItemCommand({
TableName: Table.Users.tableName, // Type-safe, no process.env string
Item: { /* ... */ },
}));
};
# SST commands
npx sst dev # Start live Lambda dev (changes deploy instantly)
npx sst deploy # Deploy to AWS
npx sst deploy --stage prod # Deploy to production
npx sst remove # Tear down all resources
npx sst console # Open SST Console (logs, DynamoDB viewer, etc.)
SST's sst dev is its standout feature. When you run it, your Lambda functions run locally but are connected to real AWS services (DynamoDB, S3, SQS, etc.). When a real request hits your deployed Lambda URL, SST tunnels it to your local machine, your code runs locally, and the response goes back. Changes you make in your editor are reflected instantly — no deployment cycle needed for development.
Cold Start Optimization
Cold starts are the main performance concern for serverless APIs:
// Techniques to reduce cold start latency
// 1. Keep Lambda warm with scheduled invocations
// In serverless.yml:
functions:
keepWarm:
handler: src/warmup.handler
events:
- schedule:
rate: rate(5 minutes)
input: { warmup: true }
// 2. Use Node.js bundling to reduce package size
// Large node_modules = slower cold starts
// Bundle with esbuild to reduce to a single file
// serverless-esbuild or esbuild in SST does this automatically
// 3. Use Lambda SnapStart (Java) or initialize outside handler
// Move DB connections outside the handler function:
const db = new DynamoDBClient({}); // Created once, reused across invocations
export const handler = async (event) => {
// db is already initialized — no connection overhead
return await db.send(/* ... */);
};
Cold start times in 2026:
- Node.js 20 (default): 100-300ms for a simple Lambda with small bundle
- Lambda with large dependencies (AWS SDK v2 full package): 500ms-1.5s
- AWS SDK v3 with modular imports: 100-400ms (much smaller bundle than v2)
- Lambda SnapStart (Java only, not Node.js): <50ms
For Node.js Lambdas serving HTTP traffic, the esbuild bundling approach (SST does this automatically) is the most effective optimization — bundle the application into a single ~1-5MB file and import only what you use from the AWS SDK.
Cost Comparison
Lambda pricing (us-east-1, arm64):
$0.0000000133 per GB-second
$0.20 per 1M requests
Example: 1M requests/month, 256MB, 100ms average:
Compute: 1,000,000 × 0.256 × 0.1 × $0.0000000133 = $0.34
Requests: 1,000,000 × $0.0000002 = $0.20
Total: ~$0.54/month
Comparison: smallest EC2 t4g.nano: $3.07/month (with no traffic savings)
At low to medium traffic, Lambda is almost always cheaper than reserved EC2. The break-even point depends on your function duration and memory requirements — for high-traffic APIs with long-running operations, EC2 or containers can become cheaper.
When to Choose
| Scenario | Pick |
|---|---|
| New AWS project, TypeScript | SST |
| Multi-cloud (AWS + GCP + Azure) | Serverless Framework |
| Complex AWS infrastructure | AWS CDK |
| Legacy Serverless Framework project | Keep SF (migration not worth it) |
| Full-stack (Lambda + Next.js + DynamoDB) | SST |
| Need Serverless Console | Serverless Framework |
| Fine-grained AWS control | AWS CDK |
Compare serverless framework package health on PkgPulse. Also see how to set up CI/CD for a JavaScript monorepo for deploying from CI and best Node.js logging libraries for observability in Lambda functions.
See the live comparison
View serverless vs. sst on PkgPulse →