You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
Building a serverless application that works in development is one thing — running it reliably in production is another. This lesson covers the operational best practices, monitoring strategies, security hardening, performance optimisation, and cost management techniques that separate prototype-quality serverless applications from production-grade systems.
Cold starts are the most common performance concern in serverless. Apply these techniques:
| Technique | Impact | Cost |
|---|---|---|
| Use lightweight runtimes (Node.js, Python) | High | Free |
| Reduce deployment package size | High | Free |
| Initialise SDK clients outside the handler | High | Free |
| Use Provisioned Concurrency | Very High | Paid |
| Avoid VPC unless necessary | Medium | Free |
| Use ARM64 (Graviton2) architecture | Medium | 20% cheaper |
// GOOD: Initialise outside the handler (runs once during INIT)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.TABLE_NAME;
// GOOD: Handler is lean — only business logic
export const handler = async (event) => {
const result = await client.send(new GetCommand({
TableName: TABLE,
Key: { userId: event.pathParameters.userId },
}));
return { statusCode: 200, body: JSON.stringify(result.Item) };
};
// BAD: Initialising inside the handler (runs EVERY invocation)
export const handler = async (event) => {
const { DynamoDBClient } = await import('@aws-sdk/client-dynamodb');
const client = new DynamoDBClient({}); // Created every time!
// ...
};
Use AWS Lambda Power Tuning to find the optimal memory setting:
Step 1: Deploy Power Tuning (SAR application)
Step 2: Run with your function ARN and a range (128-3008 MB)
Step 3: Analyse the cost/duration graph
Step 4: Set memory to the optimal value
Typical findings:
128 MB -> 2000 ms, $0.0000042 (slow and cheap per-invocation)
512 MB -> 500 ms, $0.0000042 (same cost, 4x faster!)
1024 MB -> 300 ms, $0.0000050 (slightly more expensive, still faster)
2048 MB -> 290 ms, $0.0000097 (diminishing returns)
// Enable HTTP keep-alive for AWS SDK (Node.js)
import { NodeHttpHandler } from '@smithy/node-http-handler';
import https from 'https';
const client = new DynamoDBClient({
requestHandler: new NodeHttpHandler({
httpsAgent: new https.Agent({ keepAlive: true }),
}),
});
Every function should have its own execution role with only the permissions it needs:
# GOOD: Specific permissions
Policies:
- DynamoDBReadPolicy:
TableName: !Ref UsersTable
# BAD: Overly permissive
Policies:
- Statement:
- Effect: Allow
Action: "dynamodb:*"
Resource: "*"
Never trust input — validate at every boundary:
import Ajv from 'ajv';
const ajv = new Ajv();
const schema = {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 },
},
additionalProperties: false,
};
const validate = ajv.compile(schema);
export const handler = async (event) => {
const body = JSON.parse(event.body);
if (!validate(body)) {
return {
statusCode: 400,
body: JSON.stringify({ errors: validate.errors }),
};
}
// Safe to process validated input
};
Sensitivity Level Storage Method
Low (table names) Environment variables (plain text)
Medium (API keys) SSM Parameter Store (SecureString)
High (DB passwords) Secrets Manager (with rotation)
| Control | Implementation |
|---|---|
| Authentication | Cognito, Lambda authoriser, or IAM |
| Authorisation | Check user permissions in Lambda |
| Rate limiting | API Gateway throttling + usage plans |
| Input validation | API Gateway request validators |
| WAF | AWS WAF on REST API for SQL injection, XSS protection |
| HTTPS only | API Gateway enforces HTTPS by default |
Observability
├── Metrics (What is happening?)
├── Logs (Why is it happening?)
└── Traces (Where is it happening?)
| Metric | What It Tells You | Alert When |
|---|---|---|
Invocations | Total function calls | Unexpected drop (broken trigger) |
Duration | Execution time | p99 approaching timeout |
Errors | Unhandled exceptions | Any sustained errors |
Throttles | Rejected invocations | Throttles > 0 |
ConcurrentExecutions | Active instances | Approaching account limit |
IteratorAge (streams) | Processing lag | Age increasing (falling behind) |
# SAM Template — Alert on errors
ErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmDescription: Lambda function errors
Namespace: AWS/Lambda
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !Ref MyFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
AlarmActions:
- !Ref AlertTopic
Use structured JSON logging for easier querying in CloudWatch Logs Insights:
import { Logger } from '@aws-lambda-powertools/logger';
const logger = new Logger({ serviceName: 'order-service' });
export const handler = async (event) => {
logger.info('Processing order', {
orderId: event.orderId,
customerId: event.customerId,
total: event.total,
});
try {
const result = await processOrder(event);
logger.info('Order processed successfully', { orderId: event.orderId });
return result;
} catch (error) {
logger.error('Order processing failed', {
orderId: event.orderId,
error: error.message,
stack: error.stack,
});
throw error;
}
};
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.