Serverless APIs
Deploy APIs without managing servers. Learn AWS Lambda, Google Cloud Functions, Azure Functions, and how to optimize for cold starts. Build scalable, cost-efficient APIs that scale automatically.
Why Serverless APIs?
Serverless computing eliminates infrastructure management entirely. You write code, deploy it, and the cloud provider handles everything: servers, scaling, patching, availability. Your API scales from zero to thousands of requests per second automatically. You pay only for actual execution time, no idle server costs.
- Zero infrastructure management - No servers to maintain
- Automatic scaling - From 0 to millions of requests
- Pay per execution - No cost when idle
- Built-in high availability - Multi-AZ by default
- Faster time to market - Focus on code, not ops
- Cold starts - Initial latency on first request
- Execution limits - Timeout constraints (5-15 min)
- Vendor lock-in - Platform-specific code
- Debugging complexity - Distributed tracing needed
- Not always cheaper - High traffic can cost more
AWS Lambda + API Gateway
AWS Lambda runs your code in response to events. API Gateway provides HTTP endpoints that trigger Lambda functions. Together, they create a fully managed, scalable API infrastructure with zero server management.
What is Mangum?
Mangum is an adapter that allows ASGI applications (FastAPI, Starlette) to run on AWS Lambda. It translates Lambda events (API Gateway requests) into ASGI format and vice versa. This means your FastAPI code runs unchanged on Lambda, no modifications needed.
- API Gateway receives HTTP request from client
- API Gateway invokes Lambda function with event payload
- Mangum converts Lambda event → ASGI request object
- FastAPI processes request normally (routes, dependencies, validation)
- Mangum converts ASGI response → Lambda response format
- API Gateway returns HTTP response to client
Setting Up FastAPI with Mangum
# app/main.py - FastAPI application with Lambda handler
from fastapi import FastAPI, HTTPException
from mangum import Mangum
from pydantic import BaseModel
app = FastAPI(title="Serverless API")
class Item(BaseModel):
name: str
description: str | None = None
price: float
items_db = {}
@app.get("/")
async def root():
return {"message": "Hello from Lambda!", "platform": "AWS"}
@app.get("/health")
async def health():
return {"status": "healthy"}
@app.post("/items/")
async def create_item(item: Item):
item_id = len(items_db) + 1
items_db[item_id] = item
return {"id": item_id, **item.model_dump()}
@app.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
return items_db[item_id]
# Lambda handler - Mangum wraps FastAPI app
# lifespan="off" disables startup/shutdown events (not needed in Lambda)
handler = Mangum(app, lifespan="off")
# When deployed to Lambda:
# - API Gateway invokes handler(event, context)
# - Mangum translates event → FastAPI request
# - FastAPI processes and returns response
# - Mangum translates response → API Gateway formatThat's it! The handler function is what Lambda invokes. No other changes to your FastAPI code needed. Install with: pip install mangum
AWS SAM Template for Deployment
AWS SAM (Serverless Application Model) simplifies deploying serverless applications. Define your Lambda function, API Gateway, permissions, and resources in YAML. Deploy everything with one command.
# template.yaml - SAM template
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless FastAPI application
Globals:
Function:
Timeout: 30
MemorySize: 512
Runtime: python3.11
Environment:
Variables:
ENVIRONMENT: production
LOG_LEVEL: INFO
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./
Handler: app.main.handler
Events:
# Catch-all route for all paths
ApiEvent:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: ANY
# Root path
RootEvent:
Type: HttpApi
Properties:
Path: /
Method: ANY
Policies:
# Grant DynamoDB access
- DynamoDBCrudPolicy:
TableName: !Ref ItemsTable
# Grant Systems Manager Parameter Store access
- SSMParameterReadPolicy:
ParameterName: api/*
# DynamoDB table for data persistence
ItemsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: items
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/"
FunctionArn:
Description: Lambda Function ARN
Value: !GetAtt ApiFunction.Arnsam build - Build application and dependenciessam deploy --guided - Deploy to AWS (first time, creates config)sam deploy - Deploy updatessam logs -n ApiFunction --tail - View CloudWatch logs in real-timesam delete - Remove all resourcesAWS Lambda Pricing
- Requests: $0.20 per 1 million requests
- Compute: $0.0000166667 per GB-second
- Free Tier: 1 million requests + 400,000 GB-seconds per month
Example: API with 10 million requests/month, 128 MB memory, 100ms avg duration:
• Requests: (10M - 1M free) × $0.20/1M = $1.80
• Compute: (10M × 0.1s × 0.125 GB) × $0.0000166667 = $2.08
• Total: ~$3.88/month (incredibly cheap for 10M requests!)
Google Cloud Functions with HTTP Triggers
Google Cloud Functions executes code in response to events. HTTP functions expose a URL endpoint that you can call like any API. Cloud Functions supports Python, Node.js, Go, and other runtimes. It integrates seamlessly with Google Cloud services.
Functions Framework for Python
Google's Functions Framework is an open-source library that lets you write portable serverless functions. Your function runs locally for testing and deploys to Cloud Functions without changes. It's Flask-based and simple to use.
pip install functions-frameworkfunctions-framework --target=hello --debugRuns your function locally at http://localhost:8080
Creating an HTTP Function
# main.py - Google Cloud Function with HTTP trigger
import functions_framework
from flask import jsonify, Request
import json
# In-memory storage (use Firestore in production)
items_db = {}
@functions_framework.http
def api_handler(request: Request):
"""HTTP Cloud Function entry point"""
# CORS headers (allow requests from browser)
headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
# Handle OPTIONS for CORS preflight
if request.method == 'OPTIONS':
return ('', 204, headers)
# Route requests
path = request.path
method = request.method
# GET /
if path == '/' and method == 'GET':
return (jsonify({
"message": "Hello from Google Cloud Functions!",
"platform": "Google Cloud"
}), 200, headers)
# GET /items
elif path == '/items' and method == 'GET':
return (jsonify(list(items_db.values())), 200, headers)
# POST /items
elif path == '/items' and method == 'POST':
data = request.get_json()
item_id = len(items_db) + 1
item = {"id": item_id, **data}
items_db[item_id] = item
return (jsonify(item), 201, headers)
# GET /items/{id}
elif path.startswith('/items/') and method == 'GET':
item_id = int(path.split('/')[-1])
if item_id in items_db:
return (jsonify(items_db[item_id]), 200, headers)
return (jsonify({"error": "Not found"}), 404, headers)
# 404 for unknown routes
return (jsonify({"error": "Not found"}), 404, headers)Key Differences from FastAPI: You handle routing manually. The@functions_framework.http decorator receives all requests, and you parse the path and method yourself. For complex APIs, consider using FastAPI with a wrapper.
Using FastAPI on Cloud Functions
You can run FastAPI on Cloud Functions using a simple wrapper. This gives you FastAPI's routing, validation, and OpenAPI docs while still deploying serverlessly.
# main.py - FastAPI on Google Cloud Functions
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import functions_framework
app = FastAPI(title="Cloud Functions API")
class Item(BaseModel):
name: str
price: float
items_db = {}
@app.get("/")
async def root():
return {"message": "FastAPI on Google Cloud!", "platform": "Google Cloud"}
@app.post("/items/")
async def create_item(item: Item):
item_id = len(items_db) + 1
items_db[item_id] = item.model_dump()
return {"id": item_id, **item.model_dump()}
@app.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Not found")
return items_db[item_id]
# Wrapper to convert Cloud Functions request → ASGI
@functions_framework.http
def handler(request):
from starlette.requests import Request as StarletteRequest
from starlette.responses import Response
import asyncio
# Convert Flask request to ASGI scope
scope = {
"type": "http",
"method": request.method,
"path": request.path,
"query_string": request.query_string,
"headers": [(k.lower().encode(), v.encode()) for k, v in request.headers.items()],
}
# Run FastAPI asynchronously
async def receive():
return {"type": "http.request", "body": request.get_data()}
response_started = False
status_code = 200
response_headers = []
body_parts = []
async def send(message):
nonlocal response_started, status_code, response_headers, body_parts
if message["type"] == "http.response.start":
response_started = True
status_code = message["status"]
response_headers = message.get("headers", [])
elif message["type"] == "http.response.body":
body_parts.append(message.get("body", b""))
# Execute FastAPI
asyncio.run(app(scope, receive, send))
# Return Flask response
headers = {k.decode(): v.decode() for k, v in response_headers}
return (b"".join(body_parts), status_code, headers)
# requirements.txt
# fastapi
# functions-frameworkThis wrapper converts Cloud Functions requests to ASGI format, allowing FastAPI to run unchanged. For production, consider using mangum-style adapters or sticking with native Functions Framework for better cold start performance.
Deploying to Google Cloud
# Deploy command gcloud functions deploy my-api \ --runtime python311 \ --trigger-http \ --allow-unauthenticated \ --entry-point api_handler \ --region us-central1 \ --memory 512MB \ --timeout 60s # View logs gcloud functions logs read my-api --limit 50 # Test locally first functions-framework --target=api_handler --debug
• --trigger-http: Creates HTTP endpoint
• --allow-unauthenticated: Public access (remove for auth)
• --entry-point: Function name to invoke
• --memory: 128MB to 32GB
• --timeout: Max 60 minutes (gen2), 9 minutes (gen1)
Google Cloud Functions Pricing
- Invocations: $0.40 per 1 million invocations
- Compute (Gen 2): $0.0000024 per vCPU-second + $0.00000025 per GiB-second
- Free Tier: 2 million invocations + 400,000 GB-seconds per month
Similar cost structure to AWS Lambda. Google Cloud Functions Gen 2 uses Cloud Run under the hood for better performance and longer timeouts.
Azure Functions
Azure Functions provides event-driven serverless compute for Azure. It supports multiple triggers (HTTP, Timer, Queue, Blob Storage) and bindings that simplify integration with Azure services. Azure Functions has three hosting plans: Consumption (serverless), Premium, and Dedicated (App Service).
Creating an HTTP-Triggered Function
# function_app.py - Azure Functions with HTTP trigger (v2 programming model)
import azure.functions as func
import json
import logging
app = func.FunctionApp()
items_db = {}
@app.function_name(name="health")
@app.route(route="health", methods=["GET"])
def health(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Health check endpoint called')
return func.HttpResponse(
json.dumps({"status": "healthy", "platform": "Azure"}),
mimetype="application/json",
status_code=200
)
@app.function_name(name="create_item")
@app.route(route="items", methods=["POST"])
def create_item(req: func.HttpRequest) -> func.HttpResponse:
try:
req_body = req.get_json()
item_id = len(items_db) + 1
item = {"id": item_id, **req_body}
items_db[item_id] = item
return func.HttpResponse(
json.dumps(item),
mimetype="application/json",
status_code=201
)
except ValueError:
return func.HttpResponse(
json.dumps({"error": "Invalid JSON"}),
mimetype="application/json",
status_code=400
)
@app.function_name(name="get_item")
@app.route(route="items/{item_id:int}", methods=["GET"])
def get_item(req: func.HttpRequest) -> func.HttpResponse:
item_id = int(req.route_params.get('item_id'))
if item_id in items_db:
return func.HttpResponse(
json.dumps(items_db[item_id]),
mimetype="application/json",
status_code=200
)
return func.HttpResponse(
json.dumps({"error": "Item not found"}),
mimetype="application/json",
status_code=404
)
@app.function_name(name="list_items")
@app.route(route="items", methods=["GET"])
def list_items(req: func.HttpRequest) -> func.HttpResponse:
return func.HttpResponse(
json.dumps(list(items_db.values())),
mimetype="application/json",
status_code=200
)Azure Functions v2 programming model uses decorators to define triggers and bindings. Each function is explicitly named and routed. The @app.route decorator defines HTTP routes similar to Flask.
azure-functionsLocal Development:func startRuns locally at http://localhost:7071/api/
Running FastAPI on Azure Functions
You can run FastAPI on Azure Functions using the WrapperFunction approach. This allows you to keep your FastAPI code and get all its benefits (validation, docs, dependency injection) while deploying serverlessly.
# function_app.py - FastAPI wrapper for Azure Functions
import azure.functions as func
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
# Create FastAPI app
fastapi_app = FastAPI(title="Azure Functions FastAPI")
class Item(BaseModel):
name: str
price: float
items_db = {}
@fastapi_app.get("/")
async def root():
return {"message": "FastAPI on Azure!", "platform": "Azure"}
@fastapi_app.post("/items/")
async def create_item(item: Item):
item_id = len(items_db) + 1
items_db[item_id] = item.model_dump()
return {"id": item_id, **item.model_dump()}
@fastapi_app.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Not found")
return items_db[item_id]
# Azure Functions wrapper
app = func.FunctionApp()
@app.function_name(name="fastapi_handler")
@app.route(route="{*route}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def fastapi_handler(req: func.HttpRequest) -> func.HttpResponse:
from fastapi.testclient import TestClient
client = TestClient(fastapi_app)
# Forward request to FastAPI
response = client.request(
method=req.method,
url=req.url.replace("/api", ""), # Remove /api prefix
headers=dict(req.headers),
content=req.get_body()
)
return func.HttpResponse(
response.content,
status_code=response.status_code,
headers=dict(response.headers)
)
# requirements.txt:
# azure-functions
# fastapi
# httpx # Required by TestClientDeploying to Azure
# Create function app (Consumption plan - serverless) az functionapp create \ --resource-group myResourceGroup \ --consumption-plan-location eastus \ --runtime python \ --runtime-version 3.11 \ --functions-version 4 \ --name my-api-function \ --storage-account mystorageaccount \ --os-type Linux # Deploy code func azure functionapp publish my-api-function # View logs func azure functionapp logstream my-api-function # Using VS Code Azure Functions extension (recommended): # - Install "Azure Functions" extension # - Right-click function_app.py → "Deploy to Function App" # - Select or create function app # - Deployment handles everything automatically
• Consumption: Serverless, pay per execution, auto-scales
• Premium: Pre-warmed instances (no cold starts), VNet integration
• Dedicated (App Service): Runs on existing App Service plan
Azure Functions Pricing (Consumption Plan)
- Executions: $0.20 per 1 million executions
- Execution Time: $0.000016 per GB-second
- Free Tier: 1 million executions + 400,000 GB-seconds per month
Nearly identical to AWS Lambda pricing. Premium plan offers predictable pricing with pre-warmed instances and no cold starts, starting at ~$170/month for always-on compute.
Cold Start Optimization
Cold starts are the biggest challenge with serverless functions. When a function hasn't been invoked recently, the cloud provider must initialize a new container, download code, start the runtime, and import dependencies. This adds latency to the first request.
Understanding Cold Starts
- Download code from storage
- Start runtime environment
- Import all dependencies
- Initialize connections
- Execute handler function
- Container already running
- Dependencies already loaded
- Connections established
- Execute handler only
- Near-instantaneous response
- Python FastAPI + minimal deps: 1-2 seconds
- Python FastAPI + heavy deps (NumPy, Pandas): 3-5 seconds
- Node.js Express: 200-500ms (much faster!)
- Go: 100-300ms (compiled languages win here)
Optimization Strategies
Smaller packages = faster downloads and imports. Remove unused dependencies.
# Don't import heavy libraries globally
# ❌ Bad:
import pandas as pd
import numpy as np
# ✅ Good: Lazy import
def process_data():
import pandas as pd
return pd.DataFrame(...)Move common dependencies to Lambda Layers (shared across functions).
# Create layer with common deps
mkdir python
pip install -t python/ requests boto3
zip -r layer.zip python/
# Reference layer in SAM template
Layers:
- arn:aws:lambda:...Keep N instances always warm (AWS Lambda, Google Gen 2). No cold starts!
# AWS SAM template
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 5
# Cost: ~$0.015 per instance-hour
# 5 instances = ~$50/monthPeriodically invoke function to keep it warm (free tier friendly).
# CloudWatch Events rule
rate(5 minutes) → Lambda
# In handler:
if event.get("source") == "warmer":
return "warm"
# Normal processing...More memory = more CPU = faster cold starts. Try 512MB-1024MB.
# Memory allocations:
128 MB: 0.083 vCPU (slow)
512 MB: 0.33 vCPU
1024 MB: 0.58 vCPU
1792 MB: 1 vCPU (full core)
# Often worth the cost!Reuse database connections across invocations. Initialize outside handler.
# Initialize DB connection globally
db_client = create_db_client()
def handler(event, context):
# Reuse db_client
# Connection persists across callsWhen Cold Starts Don't Matter
- Background jobs: Processing uploads, sending emails, data pipelines
- Webhooks: External systems often don't care about 2-second latency
- Internal APIs: Microservices communication where latency is acceptable
- Low-traffic APIs: If most requests are cold starts anyway, optimization won't help much
Rule of thumb: If your API gets <1 request per minute, you'll have cold starts. Either accept them, use provisioned concurrency, or switch to containers.
Serverless Framework
The Serverless Framework is an open-source tool for building and deploying serverless applications across AWS, Azure, Google Cloud, and more. It provides a unified configuration format (YAML), plugin ecosystem, and local development tools. Think of it as Terraform for serverless apps.
Why Use Serverless Framework?
- Multi-cloud: Same config for AWS, Azure, GCP
- Simple syntax: Easier than SAM/CloudFormation
- Plugin ecosystem: 1000+ plugins for everything
- Local development: Test functions offline
- Environment management: dev, staging, prod configs
- Auto-provisioning: Creates API Gateway, IAM roles, etc.
- Another abstraction: Hides cloud-specific details
- Learning curve: New syntax and concepts
- Plugin dependency: Reliance on third-party plugins
- Debugging: Generated CloudFormation can be complex
- Not always up-to-date: New AWS features take time
Installation and Project Setup
# Install Serverless Framework globally npm install -g serverless # Create new Python service serverless create --template aws-python3 --path my-api cd my-api # Or use interactive setup serverless # Install Python plugin for better packaging serverless plugin install -n serverless-python-requirements
serverless.yml Configuration
# serverless.yml - FastAPI application on AWS Lambda
service: my-api
frameworkVersion: '3'
provider:
name: aws
runtime: python3.11
region: us-east-1
stage: ${opt:stage, 'dev'}
memorySize: 512
timeout: 30
environment:
STAGE: ${self:provider.stage}
TABLE_NAME: ${self:service}-${self:provider.stage}-items
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- !GetAtt ItemsTable.Arn
functions:
api:
handler: app.main.handler
events:
- httpApi:
path: /{proxy+}
method: ANY
- httpApi:
path: /
method: ANY
resources:
Resources:
ItemsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.TABLE_NAME}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
dockerizePip: true
slim: true
strip: falseThis config creates a Lambda function with API Gateway HTTP API, DynamoDB table, and proper IAM permissions. The serverless-python-requirements plugin handles dependency packaging automatically.
FastAPI Handler (app/main.py)
# app/main.py
from fastapi import FastAPI, HTTPException
from mangum import Mangum
from pydantic import BaseModel
import os
app = FastAPI(
title="Serverless API",
version="1.0.0",
root_path=f"/{os.getenv('STAGE', 'dev')}" # For API Gateway stages
)
class Item(BaseModel):
name: str
description: str | None = None
price: float
@app.get("/")
async def root():
return {
"message": "Serverless Framework API",
"stage": os.getenv("STAGE"),
"platform": "AWS Lambda"
}
@app.get("/health")
async def health():
return {"status": "healthy"}
@app.post("/items/")
async def create_item(item: Item):
# In production, save to DynamoDB using boto3
return {"id": 1, **item.model_dump()}
@app.get("/items/{item_id}")
async def get_item(item_id: int):
# In production, fetch from DynamoDB
return {"id": item_id, "name": "Example"}
# Lambda handler
handler = Mangum(app, lifespan="off")Deployment and Management
# Deploy to dev stage (default)
serverless deploy
# Deploy to production
serverless deploy --stage prod
# Deploy only function (faster for code updates)
serverless deploy function -f api
# View logs
serverless logs -f api --tail
# Invoke function locally
serverless invoke local -f api --data '{"httpMethod":"GET","path":"/"}'
# Remove all resources
serverless remove
# Info about deployed service
serverless info
# Environment variables
serverless deploy --stage prod --param="dbHost=prod-db.example.com"1.
serverless deploy - Packages code, creates CloudFormation stack2. AWS provisions Lambda, API Gateway, DynamoDB, IAM roles
3. Function is live! Get endpoint URL from output
4.
serverless deploy function -f api - Update code only (much faster)5.
serverless remove - Clean up everything when doneMulti-Cloud: Same Code, Different Providers
Serverless Framework makes it easy to deploy the same application to different clouds. Change the provider section, adjust a few settings, and deploy.
provider:
name: aws
runtime: python3.11provider:
name: google
runtime: python311provider:
name: azure
runtime: pythonNote: While the provider changes, you may need to adjust cloud-specific resources (DynamoDB → Firestore, S3 → Cloud Storage). The function code stays the same.
Platform Comparison
Feature Comparison
| Feature | AWS Lambda | Google Cloud Functions | Azure Functions |
|---|---|---|---|
| Max Timeout | 15 minutes | 60 minutes (gen2), 9 min (gen1) | 10 minutes (Consumption) |
| Max Memory | 10 GB | 32 GB (gen2), 8 GB (gen1) | 1.5 GB (Consumption), 14 GB (Premium) |
| Package Size | 50 MB zipped, 250 MB unzipped | 100 MB (gen2), 500 MB with Cloud Storage | 1 GB (compressed) |
| Concurrency | 1,000 (soft limit, can increase) | 1,000 (can increase) | 200 per app (Consumption) |
| Cold Start | 1-3s (Python), 200-500ms (Node) | 1-2s (Python), similar to AWS | 1-3s (Python), can be higher |
| Warm Instances | Provisioned Concurrency (~$0.015/hr) | Min instances (gen2) | Premium plan (always-on) |
| Free Tier | 1M requests + 400K GB-sec/month | 2M requests + 400K GB-sec/month | 1M requests + 400K GB-sec/month |
| Pricing (Requests) | $0.20 per 1M | $0.40 per 1M | $0.20 per 1M |
| Python Runtimes | 3.9, 3.10, 3.11, 3.12, 3.13 | 3.10, 3.11, 3.12, 3.13 | 3.9, 3.10, 3.11, 3.12 |
| Local Testing | SAM CLI, LocalStack | Functions Framework | Azure Functions Core Tools |
| IaC Support | SAM, Serverless Framework, Terraform | Serverless Framework, Terraform | ARM templates, Serverless Framework |
When to Choose Each Platform
AWS Lambda
Choose if:- Already using AWS services
- Need extensive integrations (SQS, DynamoDB, S3)
- Want mature ecosystem and tooling
- Prefer AWS's security model
- Need enterprise support
- Largest serverless ecosystem
- Most third-party integrations
- Best documentation
- EventBridge for complex workflows
Google Cloud Functions
Choose if:- Using GCP services (BigQuery, Firestore)
- Need longer timeouts (60 min gen2)
- Want larger package sizes
- Prefer Google's ML/AI integrations
- Simple HTTP functions
- Generous free tier (2M requests)
- 60-minute timeout (gen2)
- Great for data pipelines
- Clean Functions Framework
Azure Functions
Choose if:- Using Azure services (Cosmos DB, Service Bus)
- Enterprise Microsoft ecosystem
- Need .NET/C# serverless
- Want strong VS Code integration
- Existing App Service infrastructure
- Premium plan (no cold starts)
- Durable Functions (stateful)
- Excellent VS Code tooling
- Flexible hosting plans
Cost Comparison: 10M Requests/Month
| Scenario | AWS Lambda | Google Cloud Functions | Azure Functions |
|---|---|---|---|
| 10M requests, 128 MB, 100ms | ~$3.88 | ~$4.40 | ~$3.88 |
| 10M requests, 512 MB, 500ms | ~$18.50 | ~$20.00 | ~$18.50 |
| With 5 warm instances (24/7) | ~$54 (Provisioned Concurrency) | ~$100 (min instances) | ~$170 (Premium plan) |
Takeaway: All three platforms have similar pricing for pure serverless (Consumption plans). Costs diverge when you need warm instances to eliminate cold starts. At very high scale (>100M requests/month), containers (ECS/Cloud Run/AKS) become cheaper.
Key Takeaways
- Serverless eliminates infrastructure management and scales automatically. Perfect for variable workloads, MVPs, and event-driven architectures. Pay only for execution.
- AWS Lambda dominates market share with the best ecosystem, integrations, and tooling. Use Mangum to run FastAPI unchanged. SAM and Serverless Framework simplify deployment.
- Google Cloud Functions excels at long-running tasks (60-minute timeout) and has a generous free tier. Functions Framework makes local testing easy. Gen 2 uses Cloud Run.
- Azure Functions integrates seamlessly with Microsoft ecosystem. Premium plan eliminates cold starts. Durable Functions enable stateful workflows. VS Code integration is excellent.
- Cold starts are the main challenge. Optimize with lazy imports, Lambda Layers, provisioned concurrency, or warming pings. Accept cold starts for background jobs and webhooks.
- Serverless Framework provides multi-cloud portability with unified YAML config, plugin ecosystem, and simplified deployment. Great for teams managing multiple cloud providers.
- Choose serverless for unpredictable traffic, microservices, and cost optimization.Use containers for consistent high traffic, low-latency requirements, or when you need full control.
- All three platforms have similar pricing (~$0.20 per 1M requests). Costs diverge with warm instances, longer timeouts, and higher memory. At massive scale, containers win on cost.
Ready to Go Serverless?
Start with AWS Lambda + Mangum if you're on AWS, or Google Cloud Functions if you prefer GCP. Azure Functions is perfect for Microsoft-centric teams. Deploy a simple FastAPI app, measure cold start times, and decide if serverless fits your use case.
Pro tip: Use serverless for new microservices and background jobs. Keep your main API on containers if you have consistent traffic. Hybrid architectures work well, serverless for spiky workloads, containers for steady-state traffic. Experiment, measure, and choose what fits your needs.