Building serverless APIs has become increasingly popular, and combining FastAPI with AWS Lambda provides a powerful, scalable solution. In this comprehensive guide, we’ll explore how to deploy FastAPI applications on AWS Lambda using Mangum, the ASGI adapter that makes this integration seamless.
Table of contents
Open Table of contents
- What is Mangum?
- Setting Up Your FastAPI Application
- Dependencies and Configuration
- Deployment Options
- Integrating with AWS Services
- Best Practices and Optimizations
- Testing Your Lambda Function
- Monitoring and Observability
- Performance Considerations
- Security Best Practices
- Troubleshooting Common Issues
- Conclusion
- Additional Resources
What is Mangum?
Mangum is an adapter for running ASGI applications in AWS Lambda. It acts as a bridge between the Lambda runtime environment and ASGI frameworks like FastAPI, allowing you to deploy your FastAPI applications as serverless functions without significant code changes.
Key Benefits
- Cost-effective: Pay only for actual usage with Lambda’s pricing model
- Auto-scaling: Automatic scaling based on demand
- No server management: Focus on code, not infrastructure
- Integration: Seamless integration with other AWS services
- Performance: Cold start optimizations and efficient request handling
Setting Up Your FastAPI Application
Let’s start by creating a FastAPI application that we’ll deploy to AWS Lambda.
Basic FastAPI App Structure
# app/main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
import json
app = FastAPI(
title="AWS Lambda FastAPI",
description="FastAPI application running on AWS Lambda with Mangum",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Pydantic models
class Item(BaseModel):
id: Optional[int] = None
name: str
description: Optional[str] = None
price: float
is_available: bool = True
class ItemResponse(BaseModel):
id: int
name: str
description: Optional[str]
price: float
is_available: bool
# In-memory storage (use DynamoDB in production)
items_db = []
next_id = 1
@app.get("/")
async def root():
return {
"message": "FastAPI on AWS Lambda with Mangum",
"version": "1.0.0",
"endpoints": {
"docs": "/docs",
"items": "/items"
}
}
@app.get("/health")
async def health_check():
return {"status": "healthy", "service": "fastapi-lambda"}
@app.get("/items", response_model=List[ItemResponse])
async def get_items():
return items_db
@app.get("/items/{item_id}", response_model=ItemResponse)
async def get_item(item_id: int):
item = next((item for item in items_db if item["id"] == item_id), None)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return item
@app.post("/items", response_model=ItemResponse, status_code=201)
async def create_item(item: Item):
global next_id
new_item = {
"id": next_id,
"name": item.name,
"description": item.description,
"price": item.price,
"is_available": item.is_available
}
items_db.append(new_item)
next_id += 1
return new_item
@app.put("/items/{item_id}", response_model=ItemResponse)
async def update_item(item_id: int, item: Item):
existing_item = next((i for i in items_db if i["id"] == item_id), None)
if not existing_item:
raise HTTPException(status_code=404, detail="Item not found")
existing_item.update({
"name": item.name,
"description": item.description,
"price": item.price,
"is_available": item.is_available
})
return existing_item
@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
global items_db
items_db = [item for item in items_db if item["id"] != item_id]
return {"message": "Item deleted successfully"}
Lambda Handler with Mangum
Create the Lambda handler using Mangum:
# lambda_function.py
from mangum import Mangum
from app.main import app
# Mangum handler
handler = Mangum(app, lifespan="off")
# For local testing
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Dependencies and Configuration
Requirements File
# requirements.txt
fastapi==0.104.1
mangum==0.17.0
pydantic==2.5.0
uvicorn==0.24.0
boto3==1.34.0
Project Structure
fastapi-lambda/
├── app/
│ ├── __init__.py
│ ├── main.py
│ └── models.py
├── lambda_function.py
├── requirements.txt
├── serverless.yml
├── Dockerfile
└── README.md
Deployment Options
Option 1: Using Serverless Framework
Install the Serverless Framework:
npm install -g serverless
npm install serverless-python-requirements
Create a serverless.yml configuration:
# serverless.yml
service: fastapi-lambda-mangum
frameworkVersion: '3'
provider:
name: aws
runtime: python3.9
region: us-east-1
stage: ${opt:stage, 'dev'}
environment:
STAGE: ${self:provider.stage}
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- "arn:aws:dynamodb:${self:provider.region}:*:table/${self:service}-${self:provider.stage}-*"
functions:
api:
handler: lambda_function.handler
timeout: 30
memorySize: 512
events:
- httpApi:
path: /{proxy+}
method: any
- httpApi:
path: /
method: any
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
dockerizePip: true
slim: true
strip: false
resources:
Resources:
ItemsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${self:provider.stage}-items
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
Deploy with:
serverless deploy --stage prod
Option 2: Using AWS SAM
Create a template.yaml for SAM:
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: FastAPI application with Mangum
Globals:
Function:
Timeout: 30
MemorySize: 512
Runtime: python3.9
Resources:
FastAPIFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./
Handler: lambda_function.handler
Events:
ApiGateway:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: any
RootPath:
Type: HttpApi
Properties:
Path: /
Method: any
Environment:
Variables:
DYNAMODB_TABLE: !Ref ItemsTable
ItemsTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
Outputs:
FastAPIApi:
Description: API Gateway endpoint URL
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/"
Deploy with SAM:
sam build
sam deploy --guided
Option 3: Using Docker and Lambda Container Images
Create a Dockerfile optimized for Lambda:
# Dockerfile
FROM public.ecr.aws/lambda/python:3.9
# Copy requirements and install dependencies
COPY requirements.txt ${LAMBDA_TASK_ROOT}
RUN pip install -r requirements.txt
# Copy application code
COPY app/ ${LAMBDA_TASK_ROOT}/app/
COPY lambda_function.py ${LAMBDA_TASK_ROOT}
# Set the CMD to your handler
CMD ["lambda_function.handler"]
Build and deploy:
# Build image
docker build -t fastapi-lambda .
# Tag for ECR
docker tag fastapi-lambda:latest <account-id>.dkr.ecr.<region>.amazonaws.com/fastapi-lambda:latest
# Push to ECR
aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com
docker push <account-id>.dkr.ecr.<region>.amazonaws.com/fastapi-lambda:latest
Integrating with AWS Services
DynamoDB Integration
Enhance your FastAPI app with DynamoDB:
# app/database.py
import boto3
from botocore.exceptions import ClientError
import os
from typing import Dict, List, Optional
class DynamoDBClient:
def __init__(self):
self.dynamodb = boto3.resource('dynamodb')
self.table_name = os.getenv('DYNAMODB_TABLE', 'fastapi-lambda-items')
self.table = self.dynamodb.Table(self.table_name)
async def create_item(self, item: Dict) -> Dict:
try:
self.table.put_item(Item=item)
return item
except ClientError as e:
raise Exception(f"Error creating item: {e}")
async def get_item(self, item_id: str) -> Optional[Dict]:
try:
response = self.table.get_item(Key={'id': item_id})
return response.get('Item')
except ClientError as e:
raise Exception(f"Error getting item: {e}")
async def get_all_items(self) -> List[Dict]:
try:
response = self.table.scan()
return response.get('Items', [])
except ClientError as e:
raise Exception(f"Error scanning items: {e}")
async def update_item(self, item_id: str, item: Dict) -> Dict:
try:
response = self.table.update_item(
Key={'id': item_id},
UpdateExpression="SET #name = :name, description = :desc, price = :price, is_available = :available",
ExpressionAttributeNames={'#name': 'name'},
ExpressionAttributeValues={
':name': item['name'],
':desc': item.get('description'),
':price': item['price'],
':available': item['is_available']
},
ReturnValues="ALL_NEW"
)
return response['Attributes']
except ClientError as e:
raise Exception(f"Error updating item: {e}")
async def delete_item(self, item_id: str) -> bool:
try:
self.table.delete_item(Key={'id': item_id})
return True
except ClientError as e:
raise Exception(f"Error deleting item: {e}")
# Initialize database client
db_client = DynamoDBClient()
Update your main application to use DynamoDB:
# Updated app/main.py (relevant parts)
from app.database import db_client
import uuid
@app.post("/items", response_model=ItemResponse, status_code=201)
async def create_item(item: Item):
new_item = {
"id": str(uuid.uuid4()),
"name": item.name,
"description": item.description,
"price": item.price,
"is_available": item.is_available
}
created_item = await db_client.create_item(new_item)
return created_item
@app.get("/items", response_model=List[ItemResponse])
async def get_items():
items = await db_client.get_all_items()
return items
@app.get("/items/{item_id}", response_model=ItemResponse)
async def get_item(item_id: str):
item = await db_client.get_item(item_id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return item
Best Practices and Optimizations
Cold Start Optimization
# Optimize imports and initialization
import os
import json
from typing import Dict, Any
# Initialize connections outside handler
def get_db_connection():
# Connection reuse across invocations
if not hasattr(get_db_connection, '_connection'):
get_db_connection._connection = boto3.resource('dynamodb')
return get_db_connection._connection
# Use connection pooling
from mangum import Mangum
from app.main import app
# Configure Mangum for better performance
handler = Mangum(
app,
lifespan="off",
api_gateway_base_path="/",
text_mime_types=[
"application/json",
"application/javascript",
"application/xml",
"application/vnd.api+json",
]
)
Environment Configuration
# app/config.py
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
app_name: str = "FastAPI Lambda"
debug: bool = False
aws_region: str = "us-east-1"
dynamodb_table: Optional[str] = None
cors_origins: list = ["*"]
class Config:
env_file = ".env"
settings = Settings()
Error Handling and Logging
# app/middleware.py
import logging
import json
from fastapi import Request, Response
from fastapi.middleware.base import BaseHTTPMiddleware
import time
# Configure logging for Lambda
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start_time = time.time()
# Log request
logger.info(f"Request: {request.method} {request.url}")
response = await call_next(request)
# Log response
process_time = time.time() - start_time
logger.info(f"Response: {response.status_code} - {process_time:.4f}s")
return response
# Add to your FastAPI app
app.add_middleware(LoggingMiddleware)
Testing Your Lambda Function
Local Testing
# test_local.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_root():
response = client.get("/")
assert response.status_code == 200
assert "FastAPI on AWS Lambda" in response.json()["message"]
def test_health_check():
response = client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "healthy"
def test_create_item():
item_data = {
"name": "Test Item",
"description": "A test item",
"price": 9.99,
"is_available": True
}
response = client.post("/items", json=item_data)
assert response.status_code == 201
assert response.json()["name"] == "Test Item"
def test_get_items():
response = client.get("/items")
assert response.status_code == 200
assert isinstance(response.json(), list)
Lambda Event Testing
# test_lambda.py
import json
from lambda_function import handler
def test_lambda_handler():
# Sample API Gateway event
event = {
"httpMethod": "GET",
"path": "/health",
"headers": {},
"queryStringParameters": None,
"body": None,
"isBase64Encoded": False
}
context = {}
response = handler(event, context)
assert response["statusCode"] == 200
body = json.loads(response["body"])
assert body["status"] == "healthy"
Monitoring and Observability
CloudWatch Integration
# app/monitoring.py
import boto3
import json
from datetime import datetime
cloudwatch = boto3.client('cloudwatch')
def put_custom_metric(metric_name: str, value: float, unit: str = 'Count'):
"""Put custom metric to CloudWatch"""
try:
cloudwatch.put_metric_data(
Namespace='FastAPI/Lambda',
MetricData=[
{
'MetricName': metric_name,
'Value': value,
'Unit': unit,
'Timestamp': datetime.utcnow()
}
]
)
except Exception as e:
print(f"Error putting metric: {e}")
# Usage in endpoints
@app.post("/items")
async def create_item(item: Item):
# ... item creation logic
put_custom_metric('ItemsCreated', 1)
return created_item
X-Ray Tracing
# Add X-Ray tracing
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.core import patch_all
# Patch AWS SDK calls
patch_all()
@xray_recorder.capture('create_item_handler')
async def create_item(item: Item):
# Your function logic here
pass
Performance Considerations
Memory and Timeout Configuration
# Serverless configuration for performance
functions:
api:
handler: lambda_function.handler
timeout: 30 # Adjust based on your needs
memorySize: 1024 # Higher memory = faster CPU
reservedConcurrency: 50 # Limit concurrent executions
provisionedConcurrency: 10 # Keep warm instances
Connection Pooling
# app/database.py (optimized)
import boto3
from botocore.config import Config
# Configure connection pooling
config = Config(
region_name='us-east-1',
retries={'max_attempts': 3},
max_pool_connections=50
)
dynamodb = boto3.resource('dynamodb', config=config)
Security Best Practices
IAM Roles and Policies
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": "arn:aws:dynamodb:*:*:table/fastapi-lambda-*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
API Authentication
# app/auth.py
from fastapi import HTTPException, Depends, Header
from typing import Optional
async def get_api_key(x_api_key: Optional[str] = Header(None)):
if not x_api_key or x_api_key != os.getenv("API_KEY"):
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
# Protect endpoints
@app.post("/items", dependencies=[Depends(get_api_key)])
async def create_item(item: Item):
# Protected endpoint logic
pass
Troubleshooting Common Issues
Cold Start Problems
- Reduce package size: Use only necessary dependencies
- Optimize imports: Import only what you need
- Use provisioned concurrency: For consistent performance
- Connection reuse: Initialize connections outside handlers
Memory Issues
# Memory optimization
import gc
from functools import lru_cache
@lru_cache(maxsize=128)
def cached_expensive_operation(param):
# Expensive operation with caching
pass
# Explicit garbage collection
def lambda_handler(event, context):
try:
result = handler(event, context)
return result
finally:
gc.collect() # Force garbage collection
API Gateway Integration Issues
# Handle different event formats
def handler(event, context):
# Handle both API Gateway v1 and v2 events
if 'version' in event and event['version'] == '2.0':
# API Gateway v2 format
pass
else:
# API Gateway v1 format
pass
return mangum_handler(event, context)
Conclusion
Deploying FastAPI applications on AWS Lambda with Mangum provides a powerful serverless solution that combines the ease of FastAPI development with the scalability and cost-effectiveness of AWS Lambda. By following the patterns and best practices outlined in this guide, you can build robust, scalable APIs that automatically handle traffic spikes while maintaining cost efficiency.
Key takeaways:
- Mangum simplifies the deployment of FastAPI to AWS Lambda
- Multiple deployment options cater to different use cases and preferences
- Proper optimization is crucial for performance and cost management
- AWS service integration enhances functionality and scalability
- Monitoring and security are essential for production deployments
Whether you’re building a simple API or a complex microservices architecture, FastAPI with Mangum on AWS Lambda provides a solid foundation for modern serverless applications.