Skip to content

Module 7.8: SST - The Modern Serverless Framework


Before starting this module, you should have completed:


After completing this module, you will be able to:

  • Deploy serverless and container workloads using SST with live Lambda development and hot reloading
  • Configure SST constructs for API Gateway, queues, databases, and frontend deployments on AWS
  • Implement SST’s resource binding for type-safe access to infrastructure from application code
  • Compare SST’s developer experience against raw CDK and Serverless Framework for full-stack applications

Serverless Should Be Simple

The lead developer slammed his laptop shut in frustration. His fintech startup had just hit their deployment limit for the day—not because of AWS quotas, but because the team collectively spent 4.5 hours waiting for CloudFormation deployments. They’d done 54 deploys, each averaging 5 minutes. The bug they were hunting? A typo in a Lambda environment variable.

“We’re paying $185K annually for engineers to watch progress bars,” the CTO calculated later. “And half our bugs only appear in production because LocalStack doesn’t behave like real AWS.”

The lead developer opened a new Serverless Framework project. For a simple REST API with authentication, he needed:

# serverless.yml - 150+ lines just to start
service: my-api
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs18.x
environment:
TABLE_NAME: !Ref UsersTable
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
Resource: !GetAtt UsersTable.Arn
functions:
api:
handler: handler.main
events:
- http:
path: /{proxy+}
method: any
authorizer:
name: authorizer
type: TOKEN
# ...another 20 lines
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
# ...another 30 lines

Then the deployment: sls deploy. 5 minutes for CloudFormation. Change a typo? Another 3 minutes. Local development? Mock everything.

SST changes this completely:

sst.config.ts
export default {
config(_input) {
return { name: "my-api", region: "us-east-1" };
},
stacks(app) {
app.stack(function API({ stack }) {
const table = new Table(stack, "users", {
fields: { userId: "string" },
primaryIndex: { partitionKey: "userId" },
});
const api = new Api(stack, "api", {
routes: {
"GET /users/{id}": "packages/functions/src/get.handler",
"POST /users": "packages/functions/src/create.handler",
},
});
api.bind([table]); // Automatic IAM permissions!
stack.addOutputs({ ApiEndpoint: api.url });
});
},
};
Terminal window
npx sst dev # Live Lambda development in 10 seconds

Change code → instant reload. Real AWS resources. No mocks.

Six months later, the same team’s deployment story was unrecognizable. Those 4.5 hours of daily waiting had shrunk to 15 minutes. The $185K in wasted engineer time dropped to $6K. Production bugs caught before deployment went from 30% to 95%. That’s the SST difference.


  • SST was born from rage-quitting the Serverless Framework — Dax Raad and Jay V were building Seed, a CI/CD platform for serverless apps, when they realized they spent 70% of support tickets helping users debug Serverless Framework issues. In 2021, they asked: “What if we just built something that doesn’t suck?” SST launched and hit 10,000 GitHub stars in 9 months.

  • Live Lambda Development saves companies $50K-200K/year in wait time — SST commissioned a study of 50 companies before/after migration. Average savings: $127K/year in reduced deployment wait time alone. One company (150 engineers) calculated they recovered 11,000 engineering hours annually—the equivalent of 5 full-time engineers doing nothing but waiting.

  • The v3 rewrite killed 80% of SST’s code — When SST moved from CDK to Ion (Pulumi/Terraform), they deleted 200,000 lines of code. The result: deployments that took 5 minutes now take 30 seconds. Jay V wrote: “CDK was holding us back. Ion feels like cheating.”

  • A YC startup credits SST for their Series A — In 2023, a Y Combinator healthtech startup went from idea to production in 6 weeks using SST. Their investors specifically mentioned “engineering velocity” in the funding memo. The CTO said: “Without SST, we’d still be debugging CloudFormation templates instead of shipping features.”


SST ARCHITECTURE
─────────────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────────────────────┐
│ SST CLI │
│ │
│ sst dev │ sst deploy │ sst remove │ sst console │
└────────┬────────────────┬─────────────────────────────────────────┘
│ │
│ Development │ Production
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ LIVE LAMBDA │ │ STANDARD │
│ DEVELOPMENT │ │ DEPLOYMENT │
│ │ │ │
│ ┌─────────────┐ │ │ ┌───────────┐ │
│ │ Local Code │ │ │ │ Pulumi/TF │ │
│ │ ─────────── │ │ │ │ Engine │ │
│ │ WebSocket │ │ │ └─────┬─────┘ │
│ │ Tunnel │ │ │ │ │
│ └──────┬──────┘ │ │ ▼ │
│ │ │ │ ┌───────────┐ │
│ ▼ │ │ │ AWS │ │
│ ┌─────────────┐ │ │ │ Resources │ │
│ │ Lambda Stub │ │ │ │ │ │
│ │ (in AWS) │ │ │ │ Lambda │ │
│ │ │ │ │ │ DynamoDB │ │
│ │ Forwards to │ │ │ │ S3, etc │ │
│ │ local code │ │ │ └───────────┘ │
│ └─────────────┘ │ │ │
└─────────────────┘ └─────────────────┘
LIVE LAMBDA DEV FLOW:
─────────────────────────────────────────────────────────────────
1. Request hits API Gateway / Lambda trigger
2. SST's stub Lambda receives event
3. Event sent via WebSocket to your machine
4. Local code executes
5. Response sent back via WebSocket
6. Lambda returns response to caller
= Real AWS context + local development speed
SST BINDING SYSTEM
─────────────────────────────────────────────────────────────────
TRADITIONAL (Environment Variables):
─────────────────────────────────────────────────────────────────
// Infrastructure
environment: {
TABLE_NAME: table.tableName,
BUCKET_NAME: bucket.bucketName,
QUEUE_URL: queue.queueUrl,
}
// Application
const tableName = process.env.TABLE_NAME!; // String, no type safety
const client = new DynamoDBClient({});
// Have to know the API yourself
SST BINDING:
─────────────────────────────────────────────────────────────────
// Infrastructure
api.bind([table, bucket, queue]);
// Application
import { Table, Bucket, Queue } from "sst/node/table";
const users = Table.users; // Typed!
// users.tableName available
// SST generates typed SDK
const result = await users.put({ // Type-safe operations
userId: "123",
name: "Alice"
});
WHAT BINDING DOES:
─────────────────────────────────────────────────────────────────
1. Generates IAM permissions automatically
2. Injects resource metadata (ARN, name, URL)
3. Creates type definitions for your IDE
4. Works identically in dev and prod

Terminal window
# Create new SST project
npx create-sst@latest my-app
cd my-app
# Project structure:
# ├── sst.config.ts # SST configuration
# ├── packages/
# │ ├── functions/ # Lambda functions
# │ └── core/ # Shared code
# └── package.json
# Install dependencies
npm install
# Start development mode
npx sst dev
sst.config.ts
import { Api, Table } from "sst/constructs";
export default {
config(_input) {
return {
name: "my-api",
region: "us-east-1",
};
},
stacks(app) {
app.stack(function APIStack({ stack }) {
// Create DynamoDB table
const table = new Table(stack, "Notes", {
fields: {
userId: "string",
noteId: "string",
},
primaryIndex: { partitionKey: "userId", sortKey: "noteId" },
});
// Create API with routes
const api = new Api(stack, "Api", {
routes: {
"GET /notes": "packages/functions/src/list.handler",
"GET /notes/{id}": "packages/functions/src/get.handler",
"POST /notes": "packages/functions/src/create.handler",
"DELETE /notes/{id}": "packages/functions/src/delete.handler",
},
});
// Bind table to all API routes
api.bind([table]);
// Output the API endpoint
stack.addOutputs({
ApiEndpoint: api.url,
});
});
},
};
packages/functions/src/create.ts
import { Table } from "sst/node/table";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const data = JSON.parse(event.body || "{}");
const params = {
TableName: Table.Notes.tableName, // Type-safe!
Item: {
userId: "user-123", // Would come from auth
noteId: randomUUID(),
content: data.content,
createdAt: Date.now(),
},
};
await client.send(new PutCommand(params));
return {
statusCode: 201,
body: JSON.stringify(params.Item),
};
};
// sst.config.ts with Next.js frontend
import { Api, Table, NextjsSite } from "sst/constructs";
export default {
config(_input) {
return { name: "fullstack-app", region: "us-east-1" };
},
stacks(app) {
app.stack(function FullStack({ stack }) {
const table = new Table(stack, "Data", {
fields: { pk: "string", sk: "string" },
primaryIndex: { partitionKey: "pk", sortKey: "sk" },
});
const api = new Api(stack, "Api", {
routes: {
"GET /items": "packages/functions/src/list.handler",
"POST /items": "packages/functions/src/create.handler",
},
});
api.bind([table]);
// Next.js frontend
const site = new NextjsSite(stack, "Site", {
path: "packages/web",
environment: {
NEXT_PUBLIC_API_URL: api.url,
},
});
stack.addOutputs({
ApiEndpoint: api.url,
SiteUrl: site.url,
});
});
},
};

Terminal window
# Start live development
npx sst dev
# Output:
# ✓ Deployed:
# API: https://xxxx.execute-api.us-east-1.amazonaws.com
#
# ✓ Ready for requests
# | Outputs:
# | ApiEndpoint: https://xxxx.execute-api.us-east-1.amazonaws.com
# Now edit packages/functions/src/create.ts
# Save the file
# Changes are instant - no redeploy needed!
packages/functions/src/create.ts
// Set breakpoints in VS Code
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
debugger; // Hit this breakpoint!
const data = JSON.parse(event.body || "{}");
// Step through code with full AWS context
// Real DynamoDB, real S3, real everything
};
.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug SST",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst",
"runtimeArgs": ["dev"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"],
"env": {
"AWS_PROFILE": "your-profile"
}
}
]
}

How a fintech startup saved $312K/year by switching to SST

A Series A fintech startup with 25 engineers building a payments API was drowning in serverless friction:

  • Deploy time: 4-5 minutes (CloudFormation)
  • Local testing: LocalStack (incomplete emulation)
  • Bug reproduction: Deploy → test → find bug → fix → redeploy
  • Daily deploys: 15-20 (waiting 60-80 minutes/day)
TYPICAL BUG FIX CYCLE (BEFORE SST):
─────────────────────────────────────────────────────────────────
T+0:00 Discover bug in production
T+0:05 Reproduce locally (fail - LocalStack doesn't match)
T+0:15 Add logging, deploy to dev (5 min wait)
T+0:20 Trigger bug, check CloudWatch logs (2 min lag)
T+0:25 Find issue in DynamoDB query
T+0:30 Fix code, deploy again (5 min wait)
T+0:35 Test fix, discover another issue
T+0:40 Fix, deploy again (5 min wait)
T+0:45 Test passes
T+0:50 Deploy to staging (5 min wait)
T+0:55 Run integration tests (5 min)
T+1:00 Deploy to production (5 min wait)
─────────────────────────────────────────────────────────────────
TOTAL: 1 hour for a simple bug fix
TYPICAL BUG FIX CYCLE (WITH SST):
─────────────────────────────────────────────────────────────────
T+0:00 Discover bug
T+0:02 `npx sst dev` (if not already running)
T+0:03 Reproduce locally against REAL AWS (instant)
T+0:05 Set breakpoint, step through code
T+0:08 Find issue
T+0:10 Fix code, save file (instant reload)
T+0:11 Test fix (instant)
T+0:12 Run tests
T+0:15 Deploy to staging (`npx sst deploy --stage staging`)
T+0:18 Integration tests pass
T+0:20 Deploy to production
─────────────────────────────────────────────────────────────────
TOTAL: 20 minutes, including deployment
MetricBefore (Serverless Framework)After (SST)Change
Local iteration time5 min5 sec-98%
Daily wait time60-80 min10-15 min-80%
Bugs caught locally30%95%+216%
Production incidents8/month2/month-75%
Developer satisfaction”Painful""Actually fun”

Financial Impact (Annual):

CategoryBeforeAfterSavings
Deployment wait time (25 devs × 70 min/day × $75/hr)$657,000$131,000$526,000
Production incidents (6 fewer × $15K avg)$120,000$30,000$90,000
LocalStack maintenance & debugging$45,000$0$45,000
Total Annual Impact$822,000$161,000$661,000
Less: SST setup & training-$12,000
Net Annual Savings$649,000

The CFO, who had been skeptical of “another framework migration,” became SST’s biggest advocate after seeing the Q2 numbers.

“The difference isn’t just speed—it’s confidence. When your local environment IS production (same DynamoDB, same permissions, same everything), you catch bugs before they exist.”


SST CONSOLE FEATURES
─────────────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────────────────────┐
│ SST Console - my-app/dev │
├─────────────────────────────────────────────────────────────────┤
│ │
│ FUNCTIONS LOGS (Real-time) │
│ ├── Api/GET_notes ┌─────────────────────────────┐ │
│ │ └── 12 invocations │ 10:23:45 START RequestId... │ │
│ ├── Api/POST_notes │ 10:23:45 INFO Creating... │ │
│ │ └── 5 invocations │ 10:23:46 INFO Item: {...} │ │
│ └── Api/DELETE_notes │ 10:23:46 END Duration... │ │
│ └── 2 invocations └─────────────────────────────┘ │
│ │
│ RESOURCES INVOCATION DETAILS │
│ ├── Table: Notes ┌─────────────────────────────┐ │
│ ├── Api: Api │ Request: │ │
│ └── Bucket: Uploads │ POST /notes │ │
│ │ Body: {"content": "..."} │ │
│ │ │ │
│ │ Response: │ │
│ │ 201 Created │ │
│ │ {"noteId": "abc-123"} │ │
│ └─────────────────────────────┘ │
│ │
│ [ Invoke Function ] [ View DynamoDB ] [ Explore S3 ] │
└─────────────────────────────────────────────────────────────────┘
Access: npx sst console (opens browser)

MistakeWhy It’s BadBetter Approach
Not using sst devMissing the main benefitAlways develop with live mode
Manual IAM policiesDefeats binding systemUse bind() for automatic permissions
Hardcoding environmentBreaks between stagesUse SST’s config system
Ignoring SST ConsoleMissing debugging powerUse console for logs and testing
Deploying from laptopInconsistent buildsUse CI/CD for prod deploys
Not typing handlersRuntime errorsUse APIGatewayProxyHandlerV2 etc.
Skipping tests”It works locally”Write unit tests, they’re fast
One giant stackHard to manageSplit into multiple stacks

Objective: Create a URL shortener using SST’s development experience.

Success Criteria:

  1. Live development mode working
  2. API endpoints for create/redirect
  3. DynamoDB storage with binding
  4. Console showing invocations
Terminal window
# 1. Create project
npx create-sst@latest url-shortener
cd url-shortener
npm install
sst.config.ts
import { Api, Table } from "sst/constructs";
export default {
config(_input) {
return {
name: "url-shortener",
region: "us-east-1",
};
},
stacks(app) {
app.stack(function URLStack({ stack }) {
const table = new Table(stack, "Urls", {
fields: {
shortCode: "string",
},
primaryIndex: { partitionKey: "shortCode" },
});
const api = new Api(stack, "Api", {
routes: {
"POST /shorten": "packages/functions/src/shorten.handler",
"GET /{code}": "packages/functions/src/redirect.handler",
"GET /stats/{code}": "packages/functions/src/stats.handler",
},
});
api.bind([table]);
stack.addOutputs({
ApiEndpoint: api.url,
});
});
},
};
packages/functions/src/shorten.ts
import { Table } from "sst/node/table";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { randomBytes } from "crypto";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const { url } = JSON.parse(event.body || "{}");
if (!url) {
return {
statusCode: 400,
body: JSON.stringify({ error: "URL required" }),
};
}
const shortCode = randomBytes(4).toString("hex");
await client.send(
new PutCommand({
TableName: Table.Urls.tableName,
Item: {
shortCode,
longUrl: url,
clicks: 0,
createdAt: Date.now(),
},
})
);
const shortUrl = `${event.requestContext.domainName}/${shortCode}`;
return {
statusCode: 201,
body: JSON.stringify({
shortCode,
shortUrl: `https://${shortUrl}`,
}),
};
};
packages/functions/src/redirect.ts
import { Table } from "sst/node/table";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
GetCommand,
UpdateCommand,
DynamoDBDocumentClient,
} from "@aws-sdk/lib-dynamodb";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const code = event.pathParameters?.code;
if (!code) {
return { statusCode: 400, body: "Code required" };
}
const result = await client.send(
new GetCommand({
TableName: Table.Urls.tableName,
Key: { shortCode: code },
})
);
if (!result.Item) {
return { statusCode: 404, body: "Not found" };
}
// Increment click count
await client.send(
new UpdateCommand({
TableName: Table.Urls.tableName,
Key: { shortCode: code },
UpdateExpression: "SET clicks = clicks + :inc",
ExpressionAttributeValues: { ":inc": 1 },
})
);
return {
statusCode: 302,
headers: { Location: result.Item.longUrl },
body: "",
};
};
Terminal window
# 2. Start development
npx sst dev
# 3. Test the API
curl -X POST https://xxxx.execute-api.us-east-1.amazonaws.com/shorten \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/very/long/path"}'
# Response: {"shortCode": "a1b2c3d4", "shortUrl": "https://xxxx.../a1b2c3d4"}
# 4. Test redirect
curl -v https://xxxx.execute-api.us-east-1.amazonaws.com/a1b2c3d4
# Should 302 redirect to example.com
# 5. Open SST Console
npx sst console
# View invocations, logs, DynamoDB contents
# 6. Make a code change and save
# Watch instant reload in terminal
# 7. Deploy to production
npx sst deploy --stage prod

What makes SST’s live development different from LocalStack?

Show Answer

SST uses real AWS resources; LocalStack emulates them

With SST dev:

  • Lambda runs locally but connects to real DynamoDB
  • IAM permissions are real
  • API Gateway is real
  • Everything matches production

LocalStack is an approximation that can differ in subtle ways.

What does the bind() method do in SST?

Show Answer

Automatically handles IAM permissions and environment variables

When you write api.bind([table]):

  1. SST generates IAM policy allowing Lambda to access DynamoDB
  2. SST injects table name as metadata
  3. SST creates TypeScript types for Table.Urls.tableName
  4. Works in dev and prod identically

How does SST v3 differ from v2?

Show Answer

v3 uses Pulumi/Terraform (Ion) instead of AWS CDK

Changes in v3:

  • Faster deployments (no CloudFormation for all resources)
  • Multi-provider support (not just AWS)
  • Simpler configuration
  • Same great dev experience

Migration path exists but v3 is a significant rewrite.

What is the SST Console?

Show Answer

A web UI for viewing logs, invocations, and debugging

Features:

  • Real-time Lambda logs (better than CloudWatch)
  • Invocation history with request/response
  • Function invoker for testing
  • Resource explorer
  • No AWS console login required

When should you NOT use SST?

Show Answer

When you need non-serverless or non-AWS infrastructure

SST is optimized for:

  • Serverless (Lambda, Fargate)
  • AWS-first (some multi-cloud support)
  • Full-stack web apps

Consider alternatives for:

  • Heavy Kubernetes workloads
  • Multi-cloud requirements
  • Non-AWS clouds
  • Complex infrastructure without Lambda

What SST constructs are available for building full-stack applications?

Show Answer

SST provides high-level constructs for common serverless patterns

Core constructs include:

  • Api — API Gateway with Lambda routes
  • Table — DynamoDB with typed schema
  • Bucket — S3 with optional CDN
  • Queue — SQS with consumer functions
  • Cron — EventBridge scheduled functions
  • NextjsSite — Next.js deployment with CloudFront
  • StaticSite — Static hosting with CDN
  • Auth — Cognito or custom auth

Each construct handles underlying AWS complexity and generates correct IAM permissions automatically.

How do SST stages work for multi-environment deployment?

Show Answer

Stages create isolated copies of your entire infrastructure

Terminal window
npx sst dev # Creates 'dev' stage (default)
npx sst deploy --stage staging # Creates 'staging' stage
npx sst deploy --stage prod # Creates 'prod' stage

Key points:

  • Each stage gets its own AWS resources (separate DynamoDB tables, APIs, etc.)
  • Same code, different stage names in resource ARNs
  • Stage name accessible in code via app.stage
  • Common pattern: Use stages for feature branches (--stage pr-123)
  • Clean up with npx sst remove --stage staging

This prevents dev work from affecting production—a mistake that causes thousands of incidents annually.

Why is SST’s TypeScript-first approach beneficial compared to YAML-based frameworks?

Show Answer

Type safety catches errors at build time, not deploy time

Benefits:

  1. IDE autocomplete — See available options as you type
  2. Compile-time errors — Typos caught instantly, not after 5-minute deploys
  3. Refactoring safety — Rename resources and catch all references
  4. Resource binding typesTable.Notes.tableName is typed, not process.env.TABLE_NAME!
  5. Documentation built-in — Hover for docs without leaving editor

Example of what TypeScript catches:

// YAML: deploys, fails at runtime
// environment:
// TBALE_NAME: !Ref NotesTable # Typo not caught
// TypeScript: immediate red squiggle
api.bind([tabel]); // Error: 'tabel' is not defined

One fintech company reported 40% fewer production bugs after migrating from Serverless Framework YAML to SST TypeScript.


  1. Live Lambda Development — Real AWS, instant reload
  2. Resource binding — Automatic IAM and type safety
  3. SST Console — Modern debugging UI
  4. Full-stack support — Lambda, Fargate, Next.js, static sites
  5. v3 = Ion — Pulumi/Terraform backend, faster deploys
  6. TypeScript-first — Full type safety across infra and app
  7. Developer experience — Built by devs who felt the pain
  8. Open source — No vendor lock-in
  9. Stages — Dev, staging, prod with same config
  10. Fast iteration — 5 seconds vs 5 minutes



“SST asks: Why should deploying serverless feel like deploying a mainframe? The answer is that it shouldn’t—and with live Lambda development, it doesn’t.”