Build Serverless Functions
Create and deploy serverless functions with AWS Lambda, Google Cloud Functions, and Azure Functions for event-driven, pay-per-use compute.
Overview
Serverless computing runs your code without you provisioning or patching a single server. You write functions, upload them to a cloud provider, and the platform handles scaling and availability on its own. You pay only for execution time — which is why it fits sporadic workloads and event-driven systems so well.
This recipe walks through building and deploying functions on the three big clouds — AWS Lambda, Google Cloud Functions, and Azure Functions — with code that runs as-is.
When to Use
Use this resource when:
- You’ve got event-driven workloads (webhooks, file processing, scheduled jobs). See Event-Driven Microservices for event-driven patterns.
- You want scaling that goes from zero to thousands of requests without you touching it. See Cold Start Optimization for minimizing startup latency.
- You need to avoid server maintenance and infrastructure overhead
- Your traffic is sporadic and provisioning servers would be wasteful. See Serverless API Gateway for building pay-per-use APIs.
Solution
Python (AWS Lambda)
import json
def handler(event, context):
# Extract query parameter
name = event.get("queryStringParameters", {}).get("name", "World")
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"message": f"Hello, {name}!"})
}
JavaScript (Google Cloud Functions)
const functions = require('@google-cloud/functions-framework');
functions.http('helloHttp', (req, res) => {
const name = req.query.name || 'World';
res.json({ message: `Hello, ${name}!` });
});
Java (Azure Functions)
import com.microsoft.azure.functions.*;
import java.util.Optional;
public class HelloFunction {
@FunctionName("hello")
public HttpResponseMessage run(
@HttpTrigger(name = "req", methods = {HttpMethod.GET}, authLevel = AuthorizationLevel.ANONYMOUS)
HttpRequestMessage<Optional<String>> request,
final ExecutionContext context) {
String name = request.getQueryParameters().getOrDefault("name", "World");
return request.createResponseBuilder(HttpStatus.OK)
.body("Hello, " + name + "!")
.build();
}
}
Explanation
Serverless platforms abstract infrastructure management:
- Event triggers: HTTP requests, file uploads, database changes, timers
- Automatic scaling: Each invocation runs in a fresh container; platforms scale containers up and down
- Pay-per-use: Billed by milliseconds of execution and number of invocations
Cold starts happen when a function sits idle: a fresh container has to boot first, adding anywhere from 100ms to 3s depending on the runtime and how much memory you assigned.
Variants
| Platform | Runtime | Trigger Types | Cold Start |
|---|---|---|---|
| AWS Lambda | Python, Node, Java, Go, Ruby | HTTP, S3, SNS, SQS, EventBridge, Cron | 100ms–1s |
| Cloud Functions | Node, Python, Go, Java | HTTP, Pub/Sub, Storage, Firestore, Cron | 200ms–2s |
| Azure Functions | Node, Python, Java, C# | HTTP, Blob, Queue, Event Grid, Timer | 200ms–3s |
What Works
- Keep functions small and focused: One function per responsibility; compose complex workflows with step functions
- Minimize deployment package size: Remove unnecessary dependencies to reduce cold start time
- Use provisioned concurrency for latency-sensitive workloads: Pre-warm containers for critical paths
- Store state externally: Functions are stateless; use DynamoDB, Redis, or Cloud Firestore for persistence
- Set memory and timeout limits appropriately: Undersized memory causes OOM; oversized wastes money
Common Mistakes
- Storing state in the function container: Local variables are lost between invocations; containers may be reused, but never depend on it
- Ignoring cold starts: User-facing synchronous APIs suffer from cold start latency
- Over-provisioning memory: Lambda allocates CPU proportionally to memory; find the sweet spot
- Not handling partial failures: Batch processing must handle retries without duplicating work
- Tight coupling to one vendor: Use abstraction layers (Serverless Framework, SAM) for portability
Advanced: Cold Start Mitigation
# AWS Lambda: initialize outside handler for connection reuse
import json
import boto3
# These run on cold start only — reused across invocations
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('users')
def handler(event, context):
# Handler stays thin — connections are already warm
user_id = event['pathParameters']['id']
response = table.get_item(Key={'id': user_id})
return {
'statusCode': 200,
'body': json.dumps(response.get('Item', {}))
}
Initialize database connections, HTTP clients, and heavy imports outside the handler function. The platform reuses the container across invocations, so these initializations run once on cold start and persist. Keep the handler body minimal — just process the event and return.
Advanced: Step Functions Orchestration
{
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:ValidateOrder",
"Next": "CheckInventory"
},
"CheckInventory": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:CheckInventory",
"Next": "ProcessPayment"
},
"ProcessPayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:ProcessPayment",
"End": true
}
}
}
Step Functions chain Lambda invocations with built-in retries, error handling, and state management — and each step retries on its own. Choice states give you conditional branching, Parallel runs steps concurrently, and Map fans out over collections.
Advanced: Provisioned Concurrency
# AWS CLI: configure provisioned concurrency
aws lambda put-provisioned-concurrency-config \
--function-name my-api \
--qualifier production \
--provisioned-concurrent-executions 10
Provisioned concurrency keeps execution environments warm and ready to respond. Configure it for latency-sensitive endpoints where cold starts are unacceptable. Monitor utilization — if provisioned capacity sits idle, reduce it. For spiky traffic, combine provisioned concurrency with on-demand scaling.
Advanced: Event-Driven Architecture
import json
import boto3
s3 = boto3.client('s3')
sns = boto3.client('sns')
def handler(event, context):
# Triggered by S3 upload
for record in event['Records']:
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
# Process uploaded file
response = s3.get_object(Bucket=bucket, Key=key)
content = response['Body'].read().decode('utf-8')
# Publish result to SNS
sns.publish(
TopicArn='arn:aws:sns:us-east-1:123:processing-complete',
Message=json.dumps({
'file': key,
'size': len(content),
'lines': content.count('\n')
})
)
return {'statusCode': 200, 'body': json.dumps({'processed': len(event['Records'])})}
Event-driven functions respond to cloud events: S3 uploads, DynamoDB streams, SQS messages, SNS notifications, or EventBridge schedules. Every event arrives carrying one or more records — your handler chews through them one by one. Design handlers to be idempotent because event sources may deliver duplicates. Use partial batch responses (Lambda) to report which records succeeded and which should be retried.
Advanced: Partial Batch Responses
SQS hands you messages in batches — if one record fails, the whole batch retries unless you say otherwise. Reporting per-record failures tells Lambda to retry only the broken ones:
import json
def handler(event, context):
failed = []
for record in event['Records']:
try:
process(record) # your business logic per message
except Exception:
# Collect the failed messageId — everything else is ACKed
failed.append({"itemIdentifier": record['messageId']})
# Only the listed items go back to the queue (then to the DLQ)
return {"batchItemFailures": failed}
The contract: the handler returns batchItemFailures with the messageIds that should be retried; everything not listed counts as consumed. Without it, one poison record drags the whole batch back — up to maxReceiveCount times before the DLQ sees it.
Advanced: Local Development
# template.yaml — AWS SAM local development
Resources:
ProcessUpload:
Type: AWS::Serverless::Function
Properties:
Handler: app.handler
Runtime: python3.11
Events:
Upload:
Type: S3
Properties:
Bucket: uploads
Events: s3:ObjectCreated:*
# Run locally:
# sam local start-api
# sam local invoke ProcessUpload --event events/s3-upload.json
# sam local generate-event s3 --bucket uploads --key data.csv > events/s3-upload.json
AWS SAM CLI, Azure Functions Core Tools, and Google Cloud Functions Emulator provide local execution environments. Generate sample events with sam local generate-event to test handler logic without deploying. Use sam local start-api to test HTTP-triggered functions with curl or Postman. Always run a final integration test in the cloud — local emulators don’t replicate all platform behaviors.
When to Avoid
- Long-running computations: Tasks exceeding 15 minutes (Lambda) need container-based compute (ECS, Cloud Run, EKS)
- Stateful applications: Real-time games, WebSocket servers, or streaming services need persistent connections
- High-frequency invocations: If your function runs 1000+ times per second, an always-on server may be cheaper
- Complex dependencies: Large native libraries or custom runtimes increase cold start and deployment complexity
Troubleshooting
- Cold start latency is high: increase provisioned concurrency, reduce package size, and avoid initializing heavy clients per invocation.
- Function times out: check downstream dependencies, memory allocation, and retry logic. Increase timeout only after optimizing the code.
- State lost between invocations: serverless functions are stateless. Persist state in a database, cache, or durable queue.
- Deployment package too large: exclude dev dependencies and unused assets.
- Event ordering issues: many event sources are at-least-once and unordered. Design for idempotency and explicit sequencing.
See Also
- Runnable companion — Lambda handler, GCF function, SAM template and a sample S3 event.
- AWS Lambda documentation — handlers, provisioned concurrency, partial batch responses.
- AWS SAM local development —
sam local invokeand event generation. - Google Cloud Functions framework — the
@google-cloud/functions-frameworkcontract. - Azure Functions for Java — annotations like
@HttpTriggerused in the example. - Complete Guide to Serverless Architecture — the long-form companion guide on this site.
Frequently Asked Questions
How do I reduce cold start latency?
Use lighter runtimes (Node.js, Python) over Java, cut the deployment package size, and initialize heavy clients outside the handler so warm containers reuse them. For user-facing paths where even that isn't enough, provisioned concurrency keeps environments pre-warmed.
Can serverless functions handle long-running tasks?
AWS Lambda caps at 15 minutes, Cloud Functions at 60, Azure is configurable per plan. For longer work, split it: put the task on SQS and let a worker function process chunks while writing progress to DynamoDB, or orchestrate short functions with Step Functions. Past an hour, Fargate or Cloud Run is a better home.
How do I debug serverless functions locally?
AWS SAM CLI, Azure Functions Core Tools, and the Functions Framework all provide local emulators — sam local invoke plus a generated event covers most handler bugs. Always re-test in the cloud though: IAM permissions, timeouts and event shapes differ from the emulator.
How do I monitor serverless functions in production?
Emit structured JSON logs (they land in CloudWatch/Stackdriver), track invocation count, duration, error rate and cold start percentage, and alarm on error rate and p99 duration. Distributed tracing (X-Ray, Cloud Trace) shows the request path across functions and downstream calls.
How do I handle retries and idempotency?
Make handlers idempotent — processing the same event twice should produce the same result — using an idempotency key like event ID plus a dedupe store. Configure a DLQ for events that exhaust retries, and on Lambda tune MaximumRetryAttempts and DestinationConfig for failed invocations.
What is the cost model for serverless?
AWS Lambda charges per invocation (~$0.20/million) plus per GB-second of execution, and the other clouds price similarly. Memory allocation, duration and invocation count all feed the bill — at sustained high traffic, compare against an always-on server before assuming serverless is cheaper.
What is the difference between Lambda layers and containers?
Layers are shared code packages (up to 5 per function) that shrink your deployment package and let you share code across functions. Container images (up to 10GB) package everything including the runtime — reach for them when your package exceeds 250MB or you need a custom runtime. Keep layers thin; large layers add cold start time.
Should I use API Gateway or direct Lambda invocation?
API Gateway when the endpoint needs auth, rate limiting, request validation or a custom domain. Direct invocation (EventBridge, SQS, SNS) for internal calls where you control both sides — API Gateway adds per-request cost and ~10-30ms latency you don't need internally.
How do I manage environment variables across stages?
Keep config per stage (serverless.dev.yml, serverless.prod.yml) and never put secrets in them — secrets live in Parameter Store or Secrets Manager, read at runtime through the function's execution role. Plain values like API URLs are fine as function environment variables.
Related Resources
Event-Driven Architecture — Queues, Topics, and Streams
A practical guide to event-driven architecture: events vs commands, message brokers, patterns like CQRS and Saga, and when to choose async over sync.
GuideSoftware Architecture Guide
A guide to designing software architecture: monoliths vs microservices, layered architecture, data flow, and technology selection criteria.
PatternObserver Pattern
Define a subscription mechanism to notify multiple objects about events. A behavioral design pattern for event-driven communication.
RecipeImplement Event Sourcing in Serverless Architectures
How to capture all changes as immutable events using event sourcing with AWS Lambda, DynamoDB streams, and event stores for audit trails and temporal queries.
RecipeMinimize Cold Start Latency in Serverless Functions
How to reduce cold start times in AWS Lambda, Azure Functions, and Cloud Run using provisioned concurrency, lazy loading, runtime tuning, and dependency optimization.