Skip to main content

Guide

SST v3 vs Serverless Framework vs AWS CDK 2026

SST v3 vs Serverless Framework v4 vs AWS CDK v2 compared for Node.js serverless infrastructure. Lambda deployment, TypeScript, local dev, DX, and cost for 2026.

·PkgPulse Team·
0

SST v3 vs Serverless Framework vs AWS CDK: Node.js IaC in 2026

TL;DR

Deploying Node.js to AWS Lambda, API Gateway, and S3 requires an infrastructure-as-code layer. SST v3 (Ion, now Pulumi-based) is the developer-first choice — opinionated, fast sst dev live Lambda debugging, automatic type-safe resource bindings, and a growing component ecosystem. Serverless Framework v4 is the classic choice — massive plugin ecosystem, 10 years of battle-testing, declarative YAML, but now has commercial licensing for teams. AWS CDK v2 is the AWS-native choice — TypeScript constructs, the most powerful and flexible option, but with a steeper learning curve and no developer experience improvements out of the box. For developer experience and speed: SST v3. For maximum plugin ecosystem and YAML declarativeness: Serverless Framework. For maximum AWS flexibility and enterprise control: AWS CDK.

Key Takeaways

  • SST v3 (Ion) is now built on Pulumi — moved from CDK in 2024, enabling multi-cloud deployments
  • Serverless Framework v4 introduced commercial licensing — free for individuals, paid for teams (>$0 revenue)
  • SST's sst dev provides live Lambda debugging — breakpoints, hot reloading, without the deploy cycle
  • AWS CDK GitHub stars: ~12k — the most comprehensive AWS IaC tool
  • SST GitHub stars: ~22k — fastest-growing opinionated serverless framework
  • All three deploy to AWS Lambda — the decision is about DX, abstractions, and ecosystem
  • SST's type bindings auto-generate TypeScript types from your infrastructure configuration

SST v3 (Ion): Developer Experience First

SST Ion (v3) rebuilt the framework on Pulumi instead of CDK, enabling faster deployments and multi-cloud support. The signature feature is sst dev — a live development mode where Lambda functions run locally but are invoked by real AWS triggers.

Installation

npx sst@latest create my-app
cd my-app
npm install

sst.config.ts — Declarative Infrastructure

// sst.config.ts — TypeScript infrastructure configuration
/// <reference path="./.sst/platform/config.d.ts" />

export default $config({
  app(input) {
    return {
      name: "my-saas-app",
      removal: input?.stage === "production" ? "retain" : "remove",
      home: "aws",
    };
  },

  async run() {
    // S3 bucket
    const bucket = new sst.aws.Bucket("UserAssets", {
      public: true,  // Public read
    });

    // RDS PostgreSQL
    const database = new sst.aws.Postgres("AppDatabase", {
      scaling: {
        min: "0 ACU",   // Scale to zero when idle
        max: "4 ACU",
      },
    });

    // Function with resource bindings
    const api = new sst.aws.Function("ApiHandler", {
      handler: "src/api/handler.handler",
      link: [bucket, database],  // Automatic type-safe bindings
      environment: {
        STAGE: $app.stage,
      },
    });

    // API Gateway
    new sst.aws.ApiGatewayV2("Api", {
      routes: {
        "GET /users": api,
        "POST /users": api,
        "GET /users/{id}": api,
      },
    });

    // Next.js app
    new sst.aws.Nextjs("Web", {
      path: "apps/web",
      link: [database, bucket],  // Web app also has type-safe access
    });

    return {
      bucketUrl: bucket.url,
    };
  },
});

Type-Safe Resource Bindings

// src/api/handler.ts — no hardcoded env vars, type-safe SST Resource
import { Resource } from "sst";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { Pool } from "pg";

const s3 = new S3Client({});
const db = new Pool({ connectionString: Resource.AppDatabase.secretArn });

export const handler = async (event: APIGatewayProxyEvent) => {
  // TypeScript knows exactly what's available
  const bucketName = Resource.UserAssets.name;         // string
  const dbArn = Resource.AppDatabase.secretArn;         // string

  await s3.send(new PutObjectCommand({
    Bucket: bucketName,
    Key: "uploads/file.jpg",
    Body: Buffer.from("content"),
  }));

  const { rows } = await db.query("SELECT * FROM users LIMIT 10");

  return {
    statusCode: 200,
    body: JSON.stringify({ users: rows }),
  };
};

sst dev — Live Lambda Development

# Start live development — Lambda runs locally, invoked by real AWS events
sst dev

# What happens:
# 1. SST deploys a "stub" Lambda to AWS (instant, no code upload)
# 2. When AWS invokes the Lambda, request is forwarded to your local machine
# 3. Your local code executes with full debugger support
# 4. Response is sent back to AWS

# Result: Real API Gateway → Real DynamoDB → Your local Lambda code
# Breakpoints work. console.log streams in real-time.
# No Docker needed. No local Lambda emulation. Real AWS.

Serverless Framework v4: The Classic Choice

Serverless Framework has deployed billions of Lambda functions since 2015. Version 4 introduced a commercial licensing model but remains the most plugin-rich option.

Installation

npm install -g serverless@v4
serverless create --template aws-nodejs-typescript --path my-service

serverless.yml Configuration

# serverless.yml
service: my-saas-app

frameworkVersion: "4"

provider:
  name: aws
  runtime: nodejs22.x
  region: us-east-1
  stage: ${opt:stage, 'dev'}
  environment:
    STAGE: ${self:provider.stage}
    DB_URL: ${ssm:/my-app/${self:provider.stage}/db-url}
    S3_BUCKET: ${self:custom.bucketName}

  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - s3:GetObject
            - s3:PutObject
          Resource: !Sub "arn:aws:s3:::${self:custom.bucketName}/*"
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:Query
          Resource: !GetAtt UsersTable.Arn

custom:
  bucketName: my-app-${self:provider.stage}-assets
  webpack:
    webpackConfig: webpack.config.js
    includeModules: true

functions:
  getUser:
    handler: src/handlers/user.get
    events:
      - httpApi:
          path: /users/{id}
          method: get

  createUser:
    handler: src/handlers/user.create
    events:
      - httpApi:
          path: /users
          method: post

  processQueue:
    handler: src/handlers/queue.process
    events:
      - sqs:
          arn: !GetAtt UserQueue.Arn
          batchSize: 10

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: users-${self:provider.stage}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH

    UserQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: user-queue-${self:provider.stage}

Handler with TypeScript

// src/handlers/user.ts
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";

const dynamo = new DynamoDBClient({ region: process.env.AWS_REGION });

export const get: APIGatewayProxyHandlerV2 = async (event) => {
  const userId = event.pathParameters?.id;

  if (!userId) {
    return { statusCode: 400, body: JSON.stringify({ error: "Missing user ID" }) };
  }

  const result = await dynamo.send(new GetItemCommand({
    TableName: `users-${process.env.STAGE}`,
    Key: marshall({ id: userId }),
  }));

  if (!result.Item) {
    return { statusCode: 404, body: JSON.stringify({ error: "User not found" }) };
  }

  return {
    statusCode: 200,
    body: JSON.stringify(unmarshall(result.Item)),
  };
};
# serverless.yml — popular plugins ecosystem
plugins:
  - serverless-esbuild         # TypeScript/ESM bundling
  - serverless-offline         # Local API Gateway emulation
  - serverless-dynamodb-local  # Local DynamoDB for testing
  - serverless-prune-plugin    # Delete old Lambda versions
  - serverless-domain-manager  # Custom domains
  - serverless-lift            # Higher-level constructs (SQS, S3 website)

AWS CDK v2: Maximum AWS Power

AWS CDK (Cloud Development Kit) is Amazon's official IaC framework. You write TypeScript constructs that synthesize into CloudFormation templates. Maximum flexibility, maximum verbosity.

Installation

npm install -g aws-cdk
npm install aws-cdk-lib constructs
cdk init app --language typescript

CDK Stack Definition

// lib/my-app-stack.ts
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigw from "aws-cdk-lib/aws-apigatewayv2";
import * as integrations from "aws-cdk-lib/aws-apigatewayv2-integrations";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

export class MyAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // S3 bucket
    const bucket = new s3.Bucket(this, "AssetsBucket", {
      bucketName: `my-app-${this.account}-assets`,
      publicReadAccess: false,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // DynamoDB table
    const usersTable = new dynamodb.Table(this, "UsersTable", {
      partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // Lambda function with automatic bundling
    const apiHandler = new NodejsFunction(this, "ApiHandler", {
      entry: "src/api/handler.ts",
      runtime: lambda.Runtime.NODEJS_22_X,
      environment: {
        USERS_TABLE: usersTable.tableName,
        ASSETS_BUCKET: bucket.bucketName,
        STAGE: this.node.tryGetContext("stage") ?? "dev",
      },
      bundling: {
        minify: true,
        externalModules: ["@aws-sdk/*"],
      },
    });

    // Grant permissions
    usersTable.grantReadWriteData(apiHandler);
    bucket.grantReadWrite(apiHandler);

    // HTTP API
    const api = new apigw.HttpApi(this, "HttpApi", {
      corsPreflight: {
        allowHeaders: ["Content-Type", "Authorization"],
        allowMethods: [apigw.CorsHttpMethod.GET, apigw.CorsHttpMethod.POST],
        allowOrigins: ["https://yourapp.com"],
      },
    });

    const lambdaIntegration = new integrations.HttpLambdaIntegration(
      "ApiIntegration",
      apiHandler
    );

    api.addRoutes({
      path: "/users",
      methods: [apigw.HttpMethod.GET, apigw.HttpMethod.POST],
      integration: lambdaIntegration,
    });

    api.addRoutes({
      path: "/users/{id}",
      methods: [apigw.HttpMethod.GET, apigw.HttpMethod.DELETE],
      integration: lambdaIntegration,
    });

    // Output API URL
    new cdk.CfnOutput(this, "ApiUrl", { value: api.url! });
  }
}

CDK Deployment

# Bootstrap (once per AWS account/region)
cdk bootstrap aws://ACCOUNT_ID/us-east-1

# Synthesize CloudFormation template
cdk synth

# Compare what will change
cdk diff

# Deploy
cdk deploy --context stage=dev

# Destroy
cdk destroy

Feature Comparison

FeatureSST v3 (Ion)Serverless Framework v4AWS CDK v2
LanguageTypeScriptYAML + JS hooksTypeScript
Backend enginePulumiCloudFormationCloudFormation
Live dev modesst dev (real AWS)sls offline (emulated)❌ No official dev mode
Type-safe resource bindings✅ Auto-generated❌ Manual env varsPartial (manual)
Plugin ecosystemGrowing✅ 1,000+ plugins✅ Constructs library
Next.js supportsst.aws.NextjsVia plugins❌ Custom construct needed
Multi-cloud✅ Pulumi backendAWS onlyAWS only
Learning curveLowLowHigh
Commercial licensingMITTeams: paidApache 2.0
GitHub stars22k46k12k
AWS service coverageCuratedComprehensiveFull AWS

Production Hardening and State Management

All three tools manage infrastructure state differently, and understanding this matters when things go wrong in production. SST v3 uses Pulumi's state backend — by default stored in S3, but configurable to Pulumi Cloud for team collaboration with locking. This is a significant shift from SST v2's CloudFormation state, meaning teams migrating from v2 to v3 cannot do a seamless in-place upgrade; resources must be re-created. Serverless Framework stores state as CloudFormation stacks, which integrates naturally with AWS's own tooling — aws cloudformation describe-stacks gives you the current deployed state without any Serverless-specific tooling. AWS CDK also targets CloudFormation, so drift detection, stack events, and rollback behavior are all handled by AWS natively. For disaster recovery planning, CDK and Serverless Framework have a decade of production war stories; SST v3 on Pulumi is newer but the underlying Pulumi engine is mature and used by enterprise customers.

Security Considerations and IAM Patterns

Each framework takes a different approach to IAM permissions. SST's link system automatically generates least-privilege IAM policies based on what resources a function is linked to — if you link a bucket, SST grants only the necessary S3 actions. Serverless Framework requires manual IAM statement configuration in serverless.yml, which is verbose but explicit. CDK's grantReadWriteData() pattern on constructs generates appropriate policies automatically, similar to SST's approach but for the full AWS service catalog. All three support VPC deployment for Lambda functions accessing private RDS or ElastiCache resources, though the configuration verbosity differs substantially — SST makes VPC nearly zero-config, while CDK gives you complete subnet and security group control. For secrets management, SST integrates with AWS Secrets Manager and SSM Parameter Store through the secret() primitive; both Serverless Framework and CDK require explicit SSM or Secrets Manager calls.

Migration Paths and Ecosystem Lock-in

Teams evaluating these tools should consider migration costs carefully. Serverless Framework's 1,000+ plugin ecosystem represents years of accumulated community investment, but plugins are only as maintained as their authors; some critical plugins like serverless-offline are community-maintained and occasionally lag behind framework versions. SST v3's move from CDK to Pulumi broke compatibility with v2 configurations entirely — a reminder that frameworks built on top of other tools inherit their breaking change risk. CDK is the most stable long-term bet since it's maintained by AWS itself and is unlikely to undergo architectural pivots, though the verbosity cost is real. For teams concerned about vendor lock-in at the framework level, CDK's direct CloudFormation output means you can always export and manage templates directly if needed.

TypeScript Integration Across Frameworks

The TypeScript story differs meaningfully between the three. SST v3 generates type bindings from your infrastructure config at build time — Resource.DatabaseName.secretArn is a typed string derived from your sst.config.ts, making it impossible to reference a resource that doesn't exist. CDK's TypeScript constructs are fully typed and published to npm with comprehensive JSDoc, making IDE autocomplete excellent across the entire AWS service catalog. Serverless Framework's YAML config is validated at deploy time, not at TypeScript compile time, meaning typos in function names or event configurations surface only during deployment. For teams using TypeScript for everything, SST and CDK offer a significantly better development experience — you get editor errors for misconfigured resources rather than failed deployments.

Cost Implications and Operational Overhead

The total cost of ownership extends beyond the tools themselves. SST v3 with Pulumi Cloud adds a managed state backend cost for teams beyond the free tier. Serverless Framework v4's commercial licensing targets teams generating revenue — the exact threshold ($0) means any production application technically requires a paid plan, though enforcement relies on the honor system currently. CDK is entirely free software with no licensing costs, though the operational overhead of building your own abstractions on top of raw constructs adds engineering time. Earthly Satellites, Depot, and similar adjacent tools can reduce the CI minutes burned on deployment pipelines regardless of which IaC tool you use. The hidden cost in all three is the time spent debugging failed deployments — SST's local development mode dramatically reduces this cycle for Lambda-heavy applications.

When to Use Each

Choose SST v3 if:

  • Developer experience is the priority — sst dev live debugging is transformative
  • You're building Next.js, Astro, or SvelteKit on AWS
  • You want automatic TypeScript types from your infrastructure config
  • Your team is building a modern SaaS and wants fast iteration

Choose Serverless Framework if:

  • You need specific plugins not available in other frameworks
  • Your team knows YAML and prefers declarative configuration
  • You have existing Serverless Framework v1/v2/v3 code to maintain
  • Individual developers (commercial licensing is free for individuals)

Choose AWS CDK if:

  • Maximum AWS service coverage and control is required
  • Your organization has CloudFormation expertise and governance requirements
  • You need to compose your own constructs for company-specific patterns
  • Multi-account, complex enterprise AWS architectures are in scope

Methodology

Data sourced from GitHub repositories (star counts as of February 2026), official documentation, npm weekly download statistics (January 2026), and community benchmarks on deployment speed from the serverless community Discord and HackerNews discussions. Feature availability verified against SST Ion v3, Serverless Framework v4, and AWS CDK v2 documentation.


Related: Coolify vs Caprover vs Dokku for self-hosted PaaS alternatives, or Temporal vs Restate vs Windmill for durable workflow orchestration on serverless.

See also: middy vs Lambda Powertools vs serverless-http 2026 and Best Serverless Frameworks for Node.js in 2026

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.