Introduction
Serverless computing has revolutionized how we build and deploy applications. By abstracting away server management, developers can focus purely on writing code while the cloud provider handles infrastructure, scaling, and availability.
This guide covers everything you need to know about building serverless applications.
What is Serverless?
Serverless Computing Definition
┌─────────────────────────────────────────────────────────────────┐
│ SERVERLESS COMPUTING │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Traditional Servers: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ You provision, manage, scale servers │ │
│ │ You pay for idle capacity │ │
│ │ You handle availability and failures │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Serverless: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Cloud provider manages servers │ │
│ │ Pay only for actual usage (per execution) │ │
│ │ Automatic scaling from zero to millions │ │
│ │ High availability built-in │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Key Characteristics: │
│ ✓ No server management │
│ ✓ Automatic scaling │
│ ✓ Pay-per-use pricing │
│ ✓ Event-driven │
│ ✓ Built-in high availability │
│ │
└─────────────────────────────────────────────────────────────────┘
Serverless vs Containers vs VMs
┌─────────────────────────────────────────────────────────────────┐
│ COMPUTE OPTIONS COMPARISON │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┬─────────────┬─────────────┬───────────────┐ │
│ │ Feature │ VMs │ Containers │ Serverless │ │
│ ├─────────────┼─────────────┼─────────────┼───────────────┤ │
│ │ Management │ Full │ Medium │ None │ │
│ │ Scaling │ Manual │ Manual/Auto│ Auto (zero-n) │ │
│ │ Pricing │ Hourly │ Per second │ Per ms │ │
│ │ Cold Start │ N/A │ Fast │ Slow │ │
│ │ Stateless │ Flexible │ Flexible │ Required │ │
│ │ Long Run │ Good │ Good │ Poor (<15min) │ │
│ │ Custom │ Full │ Full │ Limited │ │
│ │ Runtime │ Control │ Control │ Provider def │ │
│ └─────────────┴─────────────┴─────────────┴───────────────┘ │
│ │
│ When to use what: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ VMs: Legacy apps, consistent workloads, Windows │ │
│ │ Containers: Microservices, consistent performance │ │
│ │ Serverless: Event-driven, bursty, cost-sensitive │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Major Serverless Platforms
AWS Lambda
# AWS Lambda function definition (SAM template)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: My Serverless Application
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs18.x
CodeUri: ./src
MemorySize: 256
Timeout: 30
Environment:
Variables:
TABLE_NAME: !Ref MyTable
Events:
ApiEvent:
Type: Api
Properties:
Path: /items
Method: get
S3Event:
Type: S3
Properties:
Bucket: !Ref MyBucket
Event: s3:ObjectCreated:*
Layers:
- !Ref MyLayer
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MyTable
MyTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: my-table
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
// AWS Lambda - Node.js function
exports.handler = async (event, context) => {
// Event contains: headers, body, path parameters
console.log('Event:', JSON.stringify(event, null, 2));
console.log('Context:', {
functionName: context.functionName,
functionVersion: context.functionVersion,
memoryLimit: context.memoryLimit,
requestId: context.requestId
});
try {
// Your business logic
const response = {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
message: 'Hello from Lambda!',
timestamp: new Date().toISOString()
})
};
return response;
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
};
}
};
Azure Functions
// Azure Functions - C# function
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
public static class HttpTriggerFunction
{
[FunctionName("HttpTriggerFunction")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "items/{id?}")]
HttpRequest req,
ILogger log,
string id)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
if (string.IsNullOrEmpty(name))
{
return new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
return name != null
? (ActionResult)new OkObjectResult($"Hello, {name}")
: new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
}
// Azure Functions - JavaScript function
module.exports = async function (context, req) {
context.log('JavaScript HTTP trigger function processed a request.');
const name = (req.query.name || (req.body && req.body.name));
if (name) {
context.res = {
status: 200,
body: `Hello, ${name}!`
};
} else {
context.res = {
status: 400,
body: "Please pass a name on the query string or in the request body"
};
}
};
GCP Cloud Functions
// GCP Cloud Functions - 2nd Gen (Express)
const functions = require('@google-cloud/functions-framework');
functions.http('helloHttp', (req, res) => {
res.send(`Hello ${req.body.name || 'World'}!`);
});
# GCP Cloud Functions - Python
import os
def hello_world(request):
"""Responds to an HTTP request.
Args:
request (flask.Request): The request object.
<https://flask.palletsprojects.com/en/1.0.x/api/#flask.Request>
Returns:
The response text or any set of values that can be turned into a
Response object using `make_response`.
<https://flask.palletsprojects.com/en/1.0.x/api/#flask.make_response>
"""
request_json = request.get_json(silent=True) if request.is_json else {}
name = request_json.get('name', os.environ.get('NAME', 'World'))
return f'Hello {name}!'
Event-Driven Architecture
Event Sources
┌─────────────────────────────────────────────────────────────────┐
│ SERVERLESS EVENT SOURCES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ HTTP/Webhooks │ │
│ │ • API Gateway, ALB, CloudFront │ │
│ │ • Web applications, Mobile backends │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Storage Events │ │
│ │ • S3/Blob/Cloud Storage: Object created/deleted │ │
│ │ • Use cases: Image processing, backup, ETL │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Database Events │ │
│ │ • DynamoDB Streams, CosmosDB Change Feed │ │
│ │ • Use cases: Real-time sync, CDC, notifications │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Message Queues │ │
│ │ • SQS, SNS, EventBridge, Pub/Sub │ │
│ │ • Use cases: Async processing, decoupling │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Scheduled Events │ │
│ │ • CloudWatch Events, EventBridge │ │
│ │ • Use cases: Cron jobs, maintenance, batch │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ IoT Events │ │
│ │ • IoT Core, MQTT messages │ │
│ │ • Use cases: Real-time processing, alerts │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Example: Image Processing Pipeline
┌─────────────────────────────────────────────────────────────────┐
│ IMAGE PROCESSING PIPELINE (Event-Driven) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────┐ │
│ │ User │ │ S3 │ │ Lambda │ │ S3 │ │
│ │ uploads │───►│ Bucket │───►│ Function │───►│Thumbnail│ │
│ │ Image │ │ (upload) │ │ (resize)│ │ Bucket │ │
│ └──────────┘ └──────────┘ └──────────┘ └───────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ DynamoDB │ │
│ │ (metadata)│ │
│ └──────────┘ │
│ │
│ Flow: │
│ 1. User uploads image to S3 (upload event) │
│ 2. S3 triggers Lambda (object created) │
│ 3. Lambda resizes image to multiple versions │
│ 4. Lambda saves thumbnails to output bucket │
│ 5. Lambda stores metadata in DynamoDB │
│ 6. (Optional) SNS notification sent to user │
│ │
└─────────────────────────────────────────────────────────────────┘
// Lambda: Image processing function
const AWS = require('aws-sdk');
const sharp = require('sharp');
const s3 = new AWS.S3();
exports.handler = async (event) => {
// Get bucket name and file 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 image: ${bucket}/${key}`);
try {
// Download image from S3
const s3Object = await s3.getObject({
Bucket: bucket,
Key: key
}).promise();
const imageBuffer = s3Object.Body;
// Process image - create multiple sizes
const sizes = [
{ width: 1280, suffix: 'large' },
{ width: 640, suffix: 'medium' },
{ width: 320, suffix: 'small' }
];
const results = await Promise.all(
sizes.map(async ({ width, suffix }) => {
const resized = await sharp(imageBuffer)
.resize(width)
.jpeg({ quality: 80 })
.toBuffer();
const newKey = key.replace(/\.[^.]+$/, `_${suffix}.jpg`);
await s3.putObject({
Bucket: process.env.OUTPUT_BUCKET,
Key: newKey,
Body: resized,
ContentType: 'image/jpeg'
}).promise();
return { suffix, key: newKey };
})
);
// Store metadata in DynamoDB
const dynamoDB = new AWS.DynamoDB.DocumentClient();
await dynamoDB.put({
TableName: process.env.METADATA_TABLE,
Item: {
id: key,
originalKey: key,
thumbnails: results,
processedAt: new Date().toISOString()
}
}).promise();
console.log('Image processed successfully');
return { statusCode: 200 };
} catch (error) {
console.error('Error processing image:', error);
throw error;
}
};
Cold Starts
Understanding Cold Starts
┌─────────────────────────────────────────────────────────────────┐
│ COLD START EXPLAINED │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Warm Invocation: │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Request │───►│ Execute │───►│ Response │ │
│ └──────────┘ │ (ready) │ └──────────┘ │
│ └──────────┘ ~10-100ms │
│ │
│ Cold Start: │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ Request │───►│ Start │───►│ Init │───►│ Execute│ │
│ └──────────┘ │ runtime │ │ code │ │ code │ │
│ └──────────┘ └──────────┘ └────────┘ │
│ ~100-500ms ~3-10s ~10-100ms │
│ │
│ Cold Start Components: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 1. Download function code (if not cached) │ │
│ │ 2. Start runtime/VM (cloud provider) │ │
│ │ 3. Bootstrap runtime (language init) │ │
│ │ 4. Run initialization code (global scope) │ │
│ │ 5. First invocation executes │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Mitigating Cold Starts
// 1. Keep functions warm with scheduled invocations
const AWS = require('aws-sdk');
// CloudWatch Event rule triggers every 5 minutes
exports.handler = async () => {
// Do nothing, just keep function warm
console.log('Warmup ping at', new Date().toISOString());
return { statusCode: 200 };
};
// 2. Move initialization outside handler
const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();
// Initialize expensive resources once
const cachedData = await loadExpensiveData();
exports.handler = async (event) => {
// Use cached data, don't reinitialize
return { data: cachedData };
};
// 3. Use provisioned concurrency (AWS)
const AWS = require('aws-sdk');
const lambda = new AWS.Lambda();
async function warmUp() {
await lambda.putFunctionConcurrency({
FunctionName: 'my-function',
ReservedConcurrentExecutions: 10 // Keep 10 instances warm
}).promise();
}
// 4. Lazy load heavy dependencies
let heavyModule = null;
exports.handler = async (event) => {
if (!heavyModule) {
// Only load when needed
heavyModule = require('./heavy-module');
}
return heavyModule.process(event);
};
Pricing Models
Serverless Pricing Comparison
┌─────────────────────────────────────────────────────────────────┐
│ SERVERLESS PRICING COMPARISON │
├─────────────────────────────────────────────────────────────────┤
│ │
│ AWS Lambda: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Requests: $0.20 per 1M requests │ │
│ │ Duration: $0.0000166667 per GB-second │ │
│ │ Duration rounding: 1ms minimum │ │
│ │ Free tier: 1M requests + 400K GB-seconds │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ Azure Functions: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Executions: $0.20 per 1M executions (consumption) │ │
│ │ Duration: $0.000016 per GB-second │ │
│ │ Execution rounding: 1ms minimum │ │
│ │ Free tier: 1M requests + 400K GB-seconds │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ GCP Cloud Functions: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Invocations: $0.40 per 1M invocations │ │
│ │ Compute time: $0.0000125 per GB-second │ │
│ │ Duration rounding: 100ms minimum │ │
│ │ Free tier: 2M invocations + 400K GB-seconds │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ Example Cost Calculation: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 1M requests/day × 30 days = 30M requests │ │
│ │ Avg execution: 200ms × 128MB = 25.6 GB-seconds/request │ │
│ │ Total compute: 30M × 25.6 = 768M GB-seconds │ │
│ │ │ │
│ │ AWS Lambda Cost: │ │
│ │ Requests: 30 × $0.20 = $6.00 │ │
│ │ Duration: 768M × $0.0000166667 = $12.80 │ │
│ │ Total: ~$18.80/month │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Best Practices
Function Design
// Best practices for serverless functions
// 1. Stateless - use external state
const AWS = require('aws-sdk');
exports.handler = async (event, context) => {
// Good: Use DynamoDB/Redis for state
const dynamoDB = new AWS.DynamoDB.DocumentClient();
// Bad: Don't use global variables for user data
// globalUserData = fetchUser(event.userId); // ❌
// Good: Fetch user data per request
const userData = await dynamoDB.get({
TableName: 'users',
Key: { id: event.userId }
}).promise();
return userData.Item;
};
// 2. Handle errors gracefully
exports.handler = async (event) => {
try {
const result = await processEvent(event);
return { statusCode: 200, body: result };
} catch (error) {
// Don't expose internal errors
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};
// 3. Use async/await properly
exports.handler = async (event) => {
// Fire-and-forget events shouldn't await
if (event.backgroundTask) {
// Good: Don't await, return immediately
processBackground(event).catch(err => console.error(err));
return { statusCode: 202, body: 'Accepted' };
}
// Good: Await critical operations
const result = await processSync(event);
return { statusCode: 200, body: result };
};
// 4. Configure appropriate timeout
exports.handler = async (event) => {
// Set timeout based on expected duration
// Don't use 30s for quick operations
// Don't use 3s for operations taking 10s
};
Security Best Practices
# Serverless security - SAM template
AWSTemplateFormatVersion: '2010-09-09'
Resources:
SecureFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs18.x
CodeUri: ./src
# Principle of least privilege
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref DataTable
- Statement:
Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !GetAtt Secret.Arn
Environment:
Variables:
# Don't put secrets in env vars
LOG_LEVEL: info
# Enable X-Ray tracing
Tracing: Active
# VPC for sensitive functions
VpcConfig:
SecurityGroupIds: !Ref LambdaSecurityGroup
SubnetIds: !Ref PrivateSubnets
# Use secrets manager for sensitive data
Secret:
Type: AWS::SecretsManager::Secret
Properties:
SecretString: '{"api_key": "xxx"}'
Use Cases
Common Serverless Patterns
┌─────────────────────────────────────────────────────────────────┐
│ SERVERLESS USE CASES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Web Applications │ │
│ │ • REST APIs, GraphQL backends │ │
│ │ • SSR with Next.js/nuxt (Lambda@Edge) │ │
│ │ • Authentication (Cognito, Auth0) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Real-time Processing │ │
│ │ • Image/video processing │ │
│ │ • Data transformation/ETL │ │
│ │ • IoT data ingestion │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Event-Driven Applications │ │
│ │ • Webhook handlers │ │
│ │ • Chatbots │ │
│ │ • Notification systems │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Backend for Mobile/SPA │ │
│ │ • API endpoints │ │
│ │ • File upload/download │ │
│ │ • Push notifications │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Scheduled Tasks │ │
│ │ • Cron jobs │ │
│ │ • Report generation │ │
│ │ • Database cleanup │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ETL and Analytics │ │
│ │ • Data pipeline triggers │ │
│ │ • Stream processing │ │
│ │ • Analytics aggregation │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Conclusion
Serverless computing has transformed application development. Key takeaways:
- Pay-per-use - Cost scales with actual usage, not idle capacity
- Automatic scaling - From zero to millions without configuration
- Event-driven - Respond to HTTP, storage, queues, and more
- Cold starts matter - Design for latency sensitivity
- Stateless - Use external services for state and persistence
Serverless is ideal for event-driven workloads, APIs, and bursty applications. For long-running processes or consistent workloads, consider containers or VMs.
Comments