Serverless Architecture on Lambda
Lambda abstracts infrastructure but introduces new challenges: cold starts, statelessness, and per-request billing.
Basic Lambda Function
// handler.js
export const handler = async (event, context) => {
// event: API Gateway, SQS, S3, etc.
// context: Lambda runtime info
context.callbackWaitsForEmptyEventLoop = false; // Important for DB connections!
try {
const { httpMethod, path, body, headers } = event;
const payload = body ? JSON.parse(body) : null;
// Business logic
const result = await processRequest({ method: httpMethod, path, payload });
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(result),
};
} catch (err) {
console.error('Handler error:', err);
return {
statusCode: err.statusCode ?? 500,
body: JSON.stringify({ error: err.message }),
};
}
};
Cold Start Optimization
Cold starts occur when Lambda creates a new execution environment. Top causes and fixes:
// 1. Move heavy initialization outside the handler
import { DynamoDB } from '@aws-sdk/client-dynamodb';
// Executed once per cold start, not per invocation
const ddb = new DynamoDB({ region: 'us-east-1' });
const tableName = process.env.TABLE_NAME;
// 2. Lazy-load modules only when needed
let heavyLibrary;
function getHeavyLib() {
if (!heavyLibrary) {
heavyLibrary = require('./heavy-module');
}
return heavyLibrary;
}
// 3. Minimize bundle size — avoid bundling the entire AWS SDK
import { GetItemCommand } from '@aws-sdk/client-dynamodb'; // Tree-shakeable v3
Bundle Size Reduction
// serverless.js / webpack.config.js
import { nodeExternalsPlugin } from 'esbuild-node-externals';
export default {
functions: {
api: {
handler: 'src/handler.handler',
package: {
individually: true,
},
},
},
custom: {
esbuild: {
bundle: true,
minify: true,
target: 'node20',
exclude: ['@aws-sdk/*'], // Available in Lambda runtime
},
},
};
Database Connection Management
// Reuse connection pool across warm invocations
let pgPool;
async function getPool() {
if (!pgPool) {
const { Pool } = await import('pg');
pgPool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 1, // Lambda: one connection per container
idleTimeoutMillis: 0,
connectionTimeoutMillis: 5000,
});
}
return pgPool;
}
export const handler = async (event) => {
const pool = await getPool();
const client = await pool.connect();
try {
const result = await client.query('SELECT * FROM users LIMIT 10');
return { statusCode: 200, body: JSON.stringify(result.rows) };
} finally {
client.release();
}
};
Provisioned Concurrency
# serverless.yml
functions:
api:
handler: src/handler.handler
provisionedConcurrency: 5 # Keep 5 instances warm
events:
- http:
path: /api/{proxy+}
method: any
Lambda Layers
# Shared dependencies layer
layers:
dependencies:
path: layer
compatibleRuntimes:
- nodejs20.x
functions:
api:
handler: src/handler.handler
layers:
- !Ref DependenciesLambdaLayer
Container Image Lambda
FROM public.ecr.aws/lambda/nodejs:20
COPY package*.json ./
RUN npm ci --only=production
COPY src ./src
CMD ["src/handler.handler"]
Middleware Pattern (Middy)
import middy from '@middy/core';
import httpRouterHandler from '@middy/http-router';
import httpErrorHandler from '@middy/http-error-handler';
import httpJsonBodyParser from '@middy/http-json-body-parser';
import validator from '@middy/validator';
const getUsers = middy()
.use(httpJsonBodyParser())
.use(validator({ eventSchema: getUsersSchema }))
.use(httpErrorHandler())
.handler(async (event) => {
const users = await userService.list(event.queryStringParameters);
return { statusCode: 200, body: JSON.stringify(users) };
});
export const handler = httpRouterHandler([
{ method: 'GET', path: '/users', handler: getUsers },
]);
Monitoring with Lambda Powertools
import { Logger } from '@aws-lambda-powertools/logger';
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
import { Tracer } from '@aws-lambda-powertools/tracer';
const logger = new Logger({ serviceName: 'user-service' });
const metrics = new Metrics({ namespace: 'MyApp' });
const tracer = new Tracer({ serviceName: 'user-service' });
export const handler = async (event) => {
const segment = tracer.getSegment();
const subsegment = segment.addNewSubsegment('processRequest');
try {
logger.info('Processing request', { event });
const result = await processRequest(event);
metrics.addMetric('SuccessfulRequests', MetricUnits.Count, 1);
return result;
} catch (err) {
metrics.addMetric('FailedRequests', MetricUnits.Count, 1);
logger.error('Request failed', { err });
throw err;
} finally {
subsegment.close();
metrics.publishStoredMetrics();
}
};