Serverless Computing with AWS Lambda
In this lesson, we'll explore AWS Lambda, a core serverless compute service that lets you run code without provisioning or managing servers. Building on your knowledge of auto scaling and load balancing, you'll discover how Lambda takes infrastructure management to the next level by abstracting it completely.
Learning Goals:
- Understand serverless computing concepts and benefits
- Create and deploy Lambda functions
- Configure triggers and integrate with other AWS services
- Monitor and troubleshoot Lambda functions
- Apply Lambda in real-world scenarios
What is Serverless Computing?
Serverless computing allows you to build and run applications without thinking about servers. While servers are still involved, AWS manages the server infrastructure, so you can focus purely on your code.
Key characteristics:
- No server management
- Automatic scaling
- Pay-per-use pricing (you pay only for compute time)
- Built-in high availability
"Serverless" doesn't mean there are no servers—it means you don't have to manage them. AWS handles provisioning, scaling, and maintenance behind the scenes.
AWS Lambda Fundamentals
AWS Lambda executes your code in response to events and automatically manages the compute resources. Each unit of code you upload to Lambda is called a "function."
Lambda Function Components
Function: Your code package
Runtime: Execution environment (Node.js, Python, Java, etc.)
Handler: Entry point method
Event: Trigger that invokes the function
Context: Runtime information provided to handler
Creating Your First Lambda Function
Let's create a simple Lambda function that processes data:
- Node.js
- Python
exports.handler = async (event) => {
console.log('Event received:', JSON.stringify(event, null, 2));
// Process the event data
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Hello from Lambda!',
input: event,
timestamp: new Date().toISOString()
})
};
return response;
};
import json
import datetime
def lambda_handler(event, context):
print(f"Event received: {json.dumps(event)}")
# Process the event data
response = {
'statusCode': 200,
'body': json.dumps({
'message': 'Hello from Lambda!',
'input': event,
'timestamp': datetime.datetime.now().isoformat()
})
}
return response
Lambda Triggers and Event Sources
Lambda functions are invoked by triggers from various AWS services. Common triggers include:
- API Gateway - HTTP requests
- S3 - Object creation, deletion
- DynamoDB - Database stream changes
- SNS/SQS - Message notifications
- CloudWatch Events - Scheduled events
S3 Trigger Example
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.handler = async (event) => {
console.log('S3 Event:', JSON.stringify(event, null, 2));
// Process each record in the event
for (const record of event.Records) {
const bucketName = record.s3.bucket.name;
const objectKey = record.s3.object.key;
console.log(`Processing file: ${objectKey} from bucket: ${bucketName}`);
// Get the object from S3
const s3Object = await s3.getObject({
Bucket: bucketName,
Key: objectKey
}).promise();
// Process the object (e.g., resize image, extract metadata)
console.log(`File size: ${s3Object.ContentLength} bytes`);
// Your processing logic here
}
return {
statusCode: 200,
body: JSON.stringify({
message: 'S3 objects processed successfully',
processedCount: event.Records.length
})
};
};
Configuration and Best Practices
Memory and Timeout Settings
Memory: 128 MB to 10240 MB (affects CPU proportionally)
Timeout: Up to 15 minutes
Ephemeral storage: 512 MB to 10240 MB
Concurrency: Reserved and provisioned options
Start with 512MB memory for most workloads and monitor performance. Lambda charges for both memory allocation and execution time, so optimizing memory can reduce costs.
Environment Variables
exports.handler = async (event) => {
// Access environment variables
const databaseUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;
const stage = process.env.STAGE || 'development';
console.log(`Running in ${stage} environment`);
// Use environment-specific configuration
const config = {
databaseUrl: databaseUrl,
apiEndpoint: process.env.API_ENDPOINT,
debugMode: process.env.DEBUG_MODE === 'true'
};
return {
statusCode: 200,
body: JSON.stringify({
message: 'Configuration loaded',
stage: stage,
debugMode: config.debugMode
})
};
};
Monitoring and Debugging
Lambda integrates seamlessly with CloudWatch for monitoring:
exports.handler = async (event) => {
// Structured logging
console.log('INFO: Function started', {
eventType: event.type,
timestamp: new Date().toISOString(),
requestId: event.requestContext?.requestId
});
try {
// Your business logic
const result = await processData(event.data);
console.log('INFO: Processing completed successfully', {
resultSize: result.length,
processingTime: '150ms'
});
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (error) {
// Error logging
console.error('ERROR: Processing failed', {
errorMessage: error.message,
stackTrace: error.stack,
eventData: event.data
});
return {
statusCode: 500,
body: JSON.stringify({
error: 'Internal server error',
message: error.message
})
};
}
};
async function processData(data) {
// Simulate data processing
return data.map(item => ({ ...item, processed: true }));
}
Real-World Use Cases
API Backend
exports.handler = async (event) => {
const httpMethod = event.httpMethod;
const path = event.path;
const queryParams = event.queryStringParameters || {};
console.log(`Received ${httpMethod} request to ${path}`);
switch (path) {
case '/users':
if (httpMethod === 'GET') {
return await getUsers(queryParams);
}
break;
case '/orders':
if (httpMethod === 'POST') {
return await createOrder(JSON.parse(event.body));
}
break;
default:
return {
statusCode: 404,
body: JSON.stringify({ error: 'Not found' })
};
}
return {
statusCode: 405,
body: JSON.stringify({ error: 'Method not allowed' })
};
};
async function getUsers(params) {
// Simulate database query
const users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(users)
};
}
async function createOrder(orderData) {
// Validate and process order
if (!orderData.items || orderData.items.length === 0) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Order must contain items' })
};
}
// Simulate order creation
const order = {
id: Date.now(),
...orderData,
status: 'created',
createdAt: new Date().toISOString()
};
return {
statusCode: 201,
body: JSON.stringify(order)
};
}
File Processing Pipeline
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.handler = async (event) => {
try {
const results = [];
for (const record of event.Records) {
const result = await processFile(record);
results.push(result);
}
console.log(`Processed ${results.length} files successfully`);
return {
statusCode: 200,
body: JSON.stringify({
message: 'File processing completed',
results: results
})
};
} catch (error) {
console.error('File processing failed:', error);
throw error;
}
};
async function processFile(record) {
const sourceBucket = record.s3.bucket.name;
const sourceKey = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
console.log(`Processing file: ${sourceKey}`);
// Get the file from S3
const fileData = await s3.getObject({
Bucket: sourceBucket,
Key: sourceKey
}).promise();
// Process based on file type
if (sourceKey.endsWith('.json')) {
return await processJsonFile(fileData, sourceKey);
} else if (sourceKey.endsWith('.csv')) {
return await processCsvFile(fileData, sourceKey);
} else {
return {
fileName: sourceKey,
status: 'skipped',
reason: 'Unsupported file type'
};
}
}
async function processJsonFile(fileData, fileName) {
const content = JSON.parse(fileData.Body.toString());
// Transform data
const processedData = {
...content,
processedAt: new Date().toISOString(),
processedBy: 'lambda-file-processor'
};
// Save processed file
await s3.putObject({
Bucket: process.env.PROCESSED_BUCKET,
Key: `processed/${fileName}`,
Body: JSON.stringify(processedData, null, 2),
ContentType: 'application/json'
}).promise();
return {
fileName: fileName,
status: 'processed',
type: 'json',
records: Array.isArray(content) ? content.length : 1
};
}
Always consider Lambda's 15-minute execution timeout. For long-running processes, break them into smaller functions or consider alternative services like AWS Batch or ECS.
Common Pitfalls
- Cold Starts: Initial invocation may be slower due to container initialization. Use provisioned concurrency for latency-sensitive applications
- Memory Limits: Functions have limited memory (up to 10GB) and ephemeral storage (up to 10GB)
- Timeout Limits: Maximum execution time is 15 minutes
- State Management: Lambda functions are stateless—use external storage (DynamoDB, S3) for persistence
- Concurrency Limits: Account and function-level concurrency limits apply
- Dependency Size: Deployment package size affects cold start times
- Error Handling: Always implement proper error handling and logging
- Resource Cleanup: Ensure external resources (database connections) are properly closed
Summary
AWS Lambda provides a powerful serverless compute platform that automatically scales and manages infrastructure. Key takeaways:
- Lambda executes code in response to events from various AWS services
- You pay only for compute time consumed, with no charges when code isn't running
- Functions are stateless and should use external services for persistence
- Proper monitoring with CloudWatch is essential for debugging and optimization
- Consider cold starts, timeouts, and memory limits when designing applications
Serverless computing with Lambda enables you to build scalable, cost-effective applications while focusing on business logic rather than infrastructure management.
Quiz
AWS Lambda & Serverless Fundamentals
What is the maximum execution time for a Lambda function?