How to Build Serverless APIs with AWS Lambda and API Gateway
Cold start optimization and cost-effective architecture patterns
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
How to Build Serverless APIs with AWS Lambda and API Gateway
Cold Start Optimization and Cost-Effective Architecture Patterns
Serverless architecture has evolved from a buzzword into a production-grade solution powering millions of API requests daily. As a senior technical architect who has migrated dozens of monolithic APIs to serverless infrastructure, I've learned that building a serverless API with AWS Lambda and API Gateway requires more than just deploying functions—it demands understanding cold starts, cost optimization, and architectural patterns that scale.
Why Traditional Serverless Approaches Fail in 2025
The early serverless implementations (2018-2022) often suffered from critical issues that made them unsuitable for production workloads:
Naive function-per-endpoint patterns created deployment nightmares with hundreds of individual Lambda functions, each requiring separate monitoring, logging, and version management. Teams spent more time managing infrastructure than building features.
Ignoring cold start realities led to P95 latencies exceeding 3-5 seconds, particularly for Java and .NET runtimes. Users experienced inconsistent response times that damaged user experience and SLA compliance.
Lack of connection pooling meant every Lambda invocation created new database connections, quickly exhausting RDS connection limits and causing cascading failures under moderate load.
Oversized deployment packages with entire node_modules directories (100MB+) increased cold start times and deployment durations, while consuming unnecessary storage costs.
Modern serverless APIs in 2025-2026 address these issues through architectural patterns, runtime optimizations, and tooling that didn't exist in earlier iterations.
Building Your First Serverless API: The Modern Way
Let's build a production-ready serverless API using TypeScript, AWS Lambda, and API Gateway with current best practices.
Project Structure and Dependencies
// package.json
{
"name": "serverless-api-modern",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@aws-lambda-powertools/logger": "^2.0.0",
"@aws-lambda-powertools/metrics": "^2.0.0",
"@aws-lambda-powertools/tracer": "^2.0.0",
"aws-lambda": "^1.0.7",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.136",
"esbuild": "^0.20.0",
"typescript": "^5.4.0"
}
}
Optimized Lambda Handler with Middleware Pattern
// src/handlers/users.ts
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { Logger } from '@aws-lambda-powertools/logger';
import { Tracer } from '@aws-lambda-powertools/tracer';
import { z } from 'zod';
const logger = new Logger({ serviceName: 'user-api' });
const tracer = new Tracer({ serviceName: 'user-api' });
// Connection pooling - initialized outside handler
let dbConnection: any = null;
const getUserSchema = z.object({
userId: z.string().uuid()
});
// Middleware for common concerns
const withMiddleware = (handler: Function) => {
return async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> => {
try {
// Initialize DB connection once (reused across warm invocations)
if (!dbConnection) {
dbConnection = await initializeDbConnection();
}
const result = await handler(event, dbConnection);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'X-Request-Id': event.requestContext.requestId
},
body: JSON.stringify(result)
};
} catch (error) {
logger.error('Handler error', { error });
return {
statusCode: error.statusCode || 500,
body: JSON.stringify({
error: error.message,
requestId: event.requestContext.requestId
})
};
}
};
};
// Business logic handler
const getUserHandler = async (event: APIGatewayProxyEventV2, db: any) => {
const segment = tracer.getSegment();
const subsegment = segment.addNewSubsegment('getUserById');
try {
const { userId } = getUserSchema.parse({
userId: event.pathParameters?.userId
});
logger.info('Fetching user', { userId });
const user = await db.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
return { user };
} finally {
subsegment.close();
}
};
export const handler = withMiddleware(getUserHandler);
// Connection initialization with retry logic
async function initializeDbConnection() {
const { Client } = await import('pg');
const client = new Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 1, // Single connection per Lambda instance
idleTimeoutMillis: 120000
});
await client.connect();
return client;
}
Infrastructure as Code with AWS CDK
// infrastructure/api-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class ServerlessApiStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Lambda function with optimized configuration
const userFunction = new NodejsFunction(this, 'UserFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
architecture: lambda.Architecture.ARM_64, // Graviton2 for cost savings
entry: 'src/handlers/users.ts',
handler: 'handler',
memorySize: 512, // Right-sized for typical workloads
timeout: cdk.Duration.seconds(10),
bundling: {
minify: true,
sourceMap: true,
target: 'es2022',
externalModules: ['@aws-sdk/*'] // Use AWS SDK v3 from Lambda runtime
},
environment: {
NODE_OPTIONS: '--enable-source-maps',
POWERTOOLS_SERVICE_NAME: 'user-api',
LOG_LEVEL: 'INFO'
},
reservedConcurrentExecutions: 100, // Prevent runaway costs
});
// Provisioned concurrency for critical endpoints
const alias = userFunction.addAlias('live', {
provisionedConcurrentExecutions: 5 // Eliminates cold starts for baseline traffic
});
// HTTP API (cheaper and faster than REST API)
const httpApi = new apigateway.HttpApi(this, 'UserApi', {
apiName: 'user-service-api',
corsPreflight: {
allowOrigins: ['https://app.example.com'],
allowMethods: [apigateway.CorsHttpMethod.GET, apigateway.CorsHttpMethod.POST],
allowHeaders: ['Content-Type', 'Authorization'],
maxAge: cdk.Duration.days(1)
}
});
// Route integration
httpApi.addRoutes({
path: '/users/{userId}',
methods: [apigateway.HttpMethod.GET],
integration: new integrations.HttpLambdaIntegration(
'UserIntegration',
alias
)
});
}
}
Cold Start Optimization Strategies
1. Provisioned Concurrency for Critical Paths
Provisioned concurrency keeps Lambda instances warm, eliminating cold starts for your most latency-sensitive endpoints. Based on my production experience, provisioning 10-20% of peak concurrent executions provides the best cost-to-performance ratio.
2. ARM64 Architecture (Graviton2)
Switching to ARM64 reduces costs by 20% and improves cold start times by 15-20% compared to x86_64. All major Node.js dependencies support ARM64 in 2025.
3. Minimal Deployment Packages
Use esbuild or similar bundlers to tree-shake unused code. A well-optimized Lambda deployment should be under 5MB, resulting in cold starts under 500ms for Node.js runtimes.
4. Lambda SnapStart for Java
If using Java or .NET, Lambda SnapStart (now generally available) reduces cold starts from 5+ seconds to under 500ms by caching initialized snapshots.
Common Pitfalls and How to Avoid Them
Pitfall #1: Not Implementing Circuit Breakers When downstream services fail, Lambda functions retry indefinitely, consuming concurrency limits and budget. Implement circuit breakers using exponential backoff and dead-letter queues.
Pitfall #2: Synchronous Processing of Heavy Workloads Never process large files, send bulk emails, or perform complex computations synchronously in API handlers. Use SQS or EventBridge to trigger asynchronous Lambda functions.
Pitfall #3: Ignoring API Gateway Caching API Gateway caching (with TTL-based invalidation) can reduce Lambda invocations by 60-80% for read-heavy endpoints, dramatically cutting costs.
Pitfall #4: Missing CloudWatch Alarms Set up alarms for throttling, error rates, and duration. A single misconfigured Lambda can consume your entire monthly budget in hours.
Pitfall #5: Not Using Lambda Layers for Shared Dependencies Extract common dependencies (AWS SDK, logging libraries) into Lambda Layers to reduce deployment package size and improve cold start times.
Best Practices Checklist
- [ ] Use TypeScript with strict mode for type safety
- [ ] Implement structured logging with correlation IDs
- [ ] Configure reserved concurrency to prevent runaway costs
- [ ] Enable X-Ray tracing for distributed debugging
- [ ] Use HTTP API instead of REST API (50-70% cost reduction)
- [ ] Implement request validation at API Gateway level
- [ ] Set up CloudWatch dashboards for key metrics
- [ ] Use Secrets Manager for sensitive configuration
- [ ] Implement proper error handling with custom error types
- [ ] Configure VPC endpoints for private resource access
- [ ] Enable API Gateway access logging
- [ ] Use Lambda Powertools for observability
- [ ] Implement rate limiting and throttling
- [ ] Set up automated deployment pipelines with rollback
- [ ] Monitor and optimize memory allocation monthly
Frequently Asked Questions
Q: What's the typical cost of running a serverless API with 1M requests/month? A: For 1M requests with 512MB memory and 200ms average duration, expect $5-8/month for Lambda and $3-4 for API Gateway, totaling under $12/month—significantly cheaper than maintaining EC2 instances.
Q: How do I handle WebSocket connections with API Gateway? A: Use API Gateway WebSocket APIs with Lambda integrations. Store connection IDs in DynamoDB and use the API Gateway Management API to send messages to connected clients.
Q: Should I use REST API or HTTP API in API Gateway? A: Use HTTP API for new projects—it's 50-70% cheaper, has lower latency, and supports most common use cases. Only use REST API if you need API keys, request validation, or usage plans.
Q: How do I implement authentication for serverless APIs? A: Use Lambda authorizers with JWT tokens from Cognito, Auth0, or custom identity providers. Cache authorization decisions to reduce latency and costs.
Q: What's the best way to handle database connections in Lambda? A: Use RDS Proxy for connection pooling, or consider DynamoDB for serverless-native data storage. Initialize connections outside the handler function to reuse them across warm invocations.
Q: How do I test Lambda functions locally? A: Use AWS SAM CLI or LocalStack for local testing. Write unit tests that mock AWS SDK calls, and integration tests that run against actual AWS resources in a development account.
Q: What's the maximum payload size for API Gateway? A: 10MB for REST API and 6MB for HTTP API. For larger payloads, use presigned S3 URLs and have clients upload directly to S3.
Building serverless APIs with AWS Lambda and API Gateway in 2025 requires understanding modern optimization techniques, cost management strategies, and architectural patterns that have emerged from years of production experience. By following these practices and avoiding common pitfalls, you'll build APIs that scale effortlessly while maintaining predictable costs and excellent performance.