Introduction to AWS Lambda: Serverless Computing
Introduction to AWS Lambda: Serverless Computing
Introduction
In early 2017, serverless computing was beginning to transform how we think about application infrastructure. AWS Lambda, launched in 2014, had matured enough to handle production workloads. As we expanded PostPickr’s architecture beyond traditional servers, Lambda became a crucial tool for handling event-driven workloads without the operational overhead of managing EC2 instances.
This article introduces AWS Lambda and shares practical lessons from our early serverless implementations.
What is AWS Lambda?
AWS Lambda is a compute service that runs your code in response to events without requiring you to provision or manage servers. You pay only for the compute time you consume—there’s no charge when your code isn’t running.
Key Characteristics
- Event-driven execution - Triggered by AWS services or HTTP requests
- Automatic scaling - Handles 1 or 1,000,000 requests per second
- Pay-per-use - Billed in 100ms increments
- No server management - AWS handles infrastructure
- Multiple language support - Node.js, Python, Java, C#, Go
Your First Lambda Function
Let’s create a simple Lambda function that processes an S3 upload event:
// Node.js Lambda handler
exports.handler = async (event, context) => {
console.log('Event:', JSON.stringify(event, null, 2));
// Extract S3 bucket and object key from event
const bucket = event.Records[0].s3.bucket.name;
const key = decodeURIComponent(
event.Records[0].s3.object.key.replace(/\+/g, ' ')
);
console.log(`Processing file: ${key} from bucket: ${bucket}`);
try {
// Your processing logic here
// For example: resize image, process video, analyze document
return {
statusCode: 200,
body: JSON.stringify({
message: 'File processed successfully',
bucket: bucket,
key: key
})
};
} catch (error) {
console.error('Error processing file:', error);
throw error;
}
};
Lambda Execution Model
Understanding Lambda’s execution model is crucial:
Handler Function
Every Lambda has a handler function—the entry point for execution:
exports.handler = async (event, context) => {
// event: Contains data about the triggering event
// context: Provides runtime information
return {
statusCode: 200,
body: 'Response body'
};
};
Event Object
The event object structure varies by trigger source:
// API Gateway Event
{
"httpMethod": "POST",
"path": "/users",
"headers": { "Content-Type": "application/json" },
"body": "{\"name\":\"John\"}"
}
// S3 Event
{
"Records": [{
"s3": {
"bucket": { "name": "my-bucket" },
"object": { "key": "image.jpg" }
}
}]
}
// DynamoDB Stream Event
{
"Records": [{
"eventName": "INSERT",
"dynamodb": {
"NewImage": { "id": { "S": "123" } }
}
}]
}
Context Object
Provides runtime information:
exports.handler = async (event, context) => {
console.log('Request ID:', context.requestId);
console.log('Function name:', context.functionName);
console.log('Remaining time:', context.getRemainingTimeInMillis());
console.log('Memory limit:', context.memoryLimitInMB);
// Context methods
// context.getRemainingTimeInMillis() - Time before timeout
// context.callbackWaitsForEmptyEventLoop - Control callback behavior
};
Common Use Cases
1. Real-Time File Processing
Process uploads immediately after they arrive in S3:
const AWS = require('aws-sdk');
const sharp = require('sharp');
const s3 = new AWS.S3();
exports.handler = async (event) => {
const bucket = event.Records[0].s3.bucket.name;
const key = event.Records[0].s3.object.key;
// Download image from S3
const image = await s3.getObject({ Bucket: bucket, Key: key }).promise();
// Create thumbnail using sharp
const thumbnail = await sharp(image.Body)
.resize(200, 200)
.toBuffer();
// Upload thumbnail back to S3
const thumbnailKey = key.replace(/\.[^.]+$/, '_thumb.jpg');
await s3.putObject({
Bucket: bucket,
Key: thumbnailKey,
Body: thumbnail,
ContentType: 'image/jpeg'
}).promise();
return { message: 'Thumbnail created successfully' };
};
2. API Backend
Lambda can serve as your entire API backend:
exports.handler = async (event) => {
const { httpMethod, path, body } = event;
// Route based on HTTP method and path
if (httpMethod === 'GET' && path === '/users') {
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ users: await getUsers() })
};
}
if (httpMethod === 'POST' && path === '/users') {
const userData = JSON.parse(body);
const user = await createUser(userData);
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user })
};
}
return {
statusCode: 404,
body: JSON.stringify({ error: 'Not found' })
};
};
3. Scheduled Tasks
Run cron-like jobs using CloudWatch Events:
// Runs every hour to clean up old records
exports.handler = async (event) => {
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
const cutoffDate = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7 days ago
// Query items older than cutoff date
const params = {
TableName: 'TempRecords',
FilterExpression: 'created_at < :cutoff',
ExpressionAttributeValues: { ':cutoff': cutoffDate }
};
const result = await dynamodb.scan(params).promise();
// Delete old items
for (const item of result.Items) {
await dynamodb.delete({
TableName: 'TempRecords',
Key: { id: item.id }
}).promise();
}
return { deletedCount: result.Items.length };
};
4. Stream Processing
Process DynamoDB or Kinesis streams in real-time:
exports.handler = async (event) => {
for (const record of event.Records) {
if (record.eventName === 'INSERT') {
const newItem = record.dynamodb.NewImage;
// Send notification
await sendNotification({
userId: newItem.userId.S,
message: `New item created: ${newItem.id.S}`
});
}
}
return { processed: event.Records.length };
};
Best Practices
1. Keep Functions Small and Focused
Each Lambda should do one thing well:
// Good: Single responsibility
exports.processOrder = async (event) => {
const order = JSON.parse(event.body);
await validateOrder(order);
await saveOrder(order);
await sendConfirmation(order);
return { success: true };
};
// Better: Break into multiple functions
exports.validateOrder = async (event) => { /* ... */ };
exports.saveOrder = async (event) => { /* ... */ };
exports.sendConfirmation = async (event) => { /* ... */ };
2. Minimize Cold Start Impact
- Keep deployment package small - Only include necessary dependencies
- Use connection pooling - Reuse database connections
- Initialize outside handler - Reuse across invocations
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient(); // Initialized once
exports.handler = async (event) => {
// Handler code - reuses dynamodb client
const result = await dynamodb.get({
TableName: 'Users',
Key: { id: event.userId }
}).promise();
return result.Item;
};
3. Handle Errors Gracefully
exports.handler = async (event) => {
try {
const result = await processData(event);
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (error) {
console.error('Error:', error);
// Return error response instead of throwing
return {
statusCode: 500,
body: JSON.stringify({
error: 'Internal server error',
message: error.message
})
};
}
};
4. Use Environment Variables
Store configuration in environment variables:
const TABLE_NAME = process.env.TABLE_NAME;
const API_KEY = process.env.API_KEY;
exports.handler = async (event) => {
const dynamodb = new AWS.DynamoDB.DocumentClient();
const result = await dynamodb.get({
TableName: TABLE_NAME,
Key: { id: event.id }
}).promise();
return result.Item;
};
5. Set Appropriate Timeouts and Memory
- Timeout: Set based on expected execution time (max 15 minutes)
- Memory: More memory = more CPU power (128 MB to 10,240 MB)
// In AWS Console or CloudFormation
{
"Timeout": 30, // 30 seconds
"MemorySize": 512 // 512 MB
}
Monitoring and Debugging
CloudWatch Logs
All console output goes to CloudWatch Logs:
exports.handler = async (event) => {
console.log('Starting execution');
console.log('Event:', JSON.stringify(event, null, 2));
try {
const result = await processData(event);
console.log('Success:', result);
return result;
} catch (error) {
console.error('Error:', error);
throw error;
}
};
CloudWatch Metrics
Key metrics to monitor:
- Invocations - Number of times function is invoked
- Duration - Execution time
- Errors - Failed invocations
- Throttles - Rate-limited invocations
- Concurrent executions - Simultaneous runs
Cost Considerations
Lambda pricing (as of 2017):
- Free tier: 1M requests/month, 400,000 GB-seconds
- Requests: $0.20 per 1M requests
- Compute: $0.00001667 per GB-second
Example calculation:
Function: 512 MB memory, 200ms average duration
Monthly invocations: 10,000,000
Requests cost: (10M requests - 1M free) × $0.20 / 1M = $1.80
Compute cost: 10M × 0.2s × 0.5 GB × $0.00001667 = $16.67
Total: $18.47/month
Compare to EC2: A t2.small instance (1 vCPU, 2GB RAM) costs ~$17/month but runs 24/7.
Limitations to Consider
In 2017, Lambda had some constraints:
- Execution timeout: 5 minutes maximum (now 15 minutes)
- Deployment package: 50 MB (zipped), 250 MB (unzipped)
- Concurrent executions: 1,000 per account (can be increased)
- /tmp storage: 512 MB
- Cold starts: Latency on first invocation
Conclusion
AWS Lambda represents a fundamental shift in how we build and deploy applications. By eliminating server management and enabling true pay-per-use pricing, it opened new possibilities for building scalable, cost-effective systems.
At PostPickr, we started using Lambda for background tasks—processing social media webhooks, generating reports, and cleaning up expired data. The operational simplicity and automatic scaling made it a natural fit for event-driven workloads.
Key takeaways:
- Lambda is ideal for event-driven, stateless workloads
- Keep functions small, focused, and well-monitored
- Understand cold starts and optimization techniques
- Use environment variables for configuration
- Monitor costs and set appropriate memory/timeout settings
As serverless computing matures, Lambda is becoming a cornerstone of modern cloud architecture.
| *Posted on January 19, 2017 | Tags: AWS, Lambda, Serverless | Category: How-To* |