Introduction
The Serverless Framework provides a unified experience for deploying functions, APIs, and event-driven architectures across major cloud providers. While serverless eliminates infrastructure management, it introduces challenges around cold starts, observability, and cost control. This guide walks through taking a serverless application from development to production using the Serverless Framework on AWS Lambda.
Project Setup and Structure
A well-structured serverless project separates concerns across functions, layers, and configuration:
# serverless.yml
service: order-processor
frameworkVersion: "4"
provider:
name: aws
runtime: nodejs20.x
region: us-east-1
stage: ${opt:stage, 'dev'}
environment:
ORDER_TABLE: ${self:custom.tableName}
QUEUE_URL: !Ref OrderQueue
plugins:
- serverless-webpack
- serverless-offline
- serverless-prune-plugin
custom:
tableName: orders-${self:provider.stage}
webpack:
packager: pnpm
excludeFiles: src/**/*.test.ts
prune:
automatic: true
number: 3
functions:
createOrder:
handler: src/handlers/createOrder.handler
events:
- httpApi:
method: POST
path: /orders
timeout: 10
memorySize: 256
iamRoleStatements:
- Effect: Allow
Action: dynamodb:PutItem
Resource: !GetAtt OrdersTable.Arn
Infrastructure as Code
Define resources alongside functions for self-documenting infrastructure:
resources:
Resources:
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: orders-${self:provider.stage}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: orderId
AttributeType: S
- AttributeName: status
AttributeType: S
KeySchema:
- AttributeName: orderId
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: StatusIndex
KeySchema:
- AttributeName: status
KeyType: HASH
Projection:
ProjectionType: ALL
OrderQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: orders-${self:provider.stage}
VisibilityTimeout: 60
RedrivePolicy:
deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn
maxReceiveCount: 3
DeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: orders-dlq-${self:provider.stage}
Lambda Handler Implementation
Write handlers with proper error handling and observability:
// src/handlers/createOrder.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";
import { Logger } from "@aws-lambda-powertools/logger";
import { Metrics } from "@aws-lambda-powertools/metrics";
import { Tracer } from "@aws-lambda-powertools/tracer";
const logger = new Logger({ serviceName: "order-processor" });
const metrics = new Metrics({ namespace: "OrderProcessor" });
const tracer = new Tracer({ serviceName: "order-processor" });
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const body = JSON.parse(event.body || "{}");
const orderId = randomUUID();
const order = {
orderId,
...body,
status: "PENDING",
createdAt: new Date().toISOString(),
};
await ddb.send(new PutCommand({
TableName: process.env.ORDER_TABLE,
Item: order,
}));
metrics.addMetric("OrderCreated", 1, "Count");
logger.info("Order created", { orderId });
return {
statusCode: 201,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderId, status: "PENDING" }),
};
} catch (error) {
logger.error("Failed to create order", { error });
metrics.addMetric("OrderCreationError", 1, "Count");
return {
statusCode: 500,
body: JSON.stringify({ message: "Internal server error" }),
};
}
};
Local Development
The `serverless-offline` plugin provides a local Lambda emulator:
# Start local API Gateway emulator
serverless offline --stage dev --httpPort 4000
# Invoke a function directly
serverless invoke local --function createOrder \
--path test/fixtures/create-order.json
# Run with warm container simulation
serverless offline --stage dev \
--noPrependStageInUrl \
--reloadHandler
Cold Start Optimization
Cold starts add latency when Lambda scales up a new execution environment:
# Optimize for cold starts
provider:
# Use AWS Graviton for better price/performance
architecture: arm64
# Increase memory speeds up CPU allocation
# (and proportionally reduces cold start time)
memorySize: 1024
functions:
latencyCritical:
handler: src/handlers/critical.handler
# Provisioned concurrency for critical paths
provisionedConcurrency: 5
# Reserve concurrency to prevent throttling
reservedConcurrency: 20
Code-level optimizations:
// Cold start optimization techniques
// 1. Lazy initialization outside handler (reused across invocations)
let client: DynamoDBDocumentClient;
function getClient(): DynamoDBDocumentClient {
if (!client) {
client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
}
return client;
}
// 2. Bundle and tree-shake dependencies
// Use esbuild or webpack to exclude unused SDK clients
// 3. Minimize deployment package size
// Exclude in webpack:
// externals: ["@aws-sdk/client-dynamodb"]
// 4. Use AWS SDK v3 for modular imports
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
// Instead of: import { DynamoDB } from "aws-sdk";
Monitoring and Observability
CloudWatch is the default, but Powertools enhances observability significantly:
functions:
processOrder:
handler: src/handlers/processOrder.handler
environment:
POWERTOOLS_SERVICE_NAME: order-processor
POWERTOOLS_METRICS_NAMESPACE: OrderProcessor
LOG_LEVEL: INFO
events:
- sqs:
arn: !GetAtt OrderQueue.Arn
batchSize: 10
maximumBatchingWindowInSeconds: 5
Create CloudWatch dashboards and alarms:
resources:
Resources:
OrderErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: order-processor-errors-${self:provider.stage}
MetricName: Errors
Namespace: AWS/Lambda
Dimensions:
- Name: FunctionName
Value: !Ref ProcessOrderLambdaFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 5
ComparisonOperator: GreaterThanThreshold
Cost Analysis
Serverless costs are driven by invocation count, duration, and memory allocation:
# Calculate monthly cost estimate
# Invocations: 10M/month
# Avg duration: 200ms
# Memory: 1024MB
# Cost = 10M * (0.2s / 1000ms) * 1GB * $0.00001667/GB-second
# = 10M * 0.2 * 0.00001667
# = ~$33.34/month
Optimize cost with `serverless-prune-plugin` to remove old versions:
custom:
prune:
automatic: true
number: 3 # Keep only 3 latest versions
For high-throughput workloads, compare Lambda cost against ECS Fargate at sustained traffic levels. Lambda often wins for variable, low-volume traffic but becomes expensive at steady-state high throughput. Use these patterns to build production-grade serverless applications that are cost-effective, observable, and performant from day one.