FastAPI on AWS Lambda: Deploy with Mangum & API Gateway
FastAPI on AWS Lambda lets you run a fully serverless Python API with no servers to manage, automatic scaling, and pay-per-request pricing. The key piece is Mangum, an ASGI adapter that translates Lambda events from API Gateway into the format FastAPI expects. Add an API Gateway HTTP API in front and you have a production-capable serverless API in a single Lambda function. This guide walks through the full setup: app code, packaging, deployment, and the production considerations you need before going live. If you are new to Lambda, read the Lambda overview first to understand the execution model and its limits. Lambda is not the right choice for every API. If yours needs persistent connections, sustained high throughput, or sub-50ms cold-start latency, this guide covers that too.
How it works in plain terms
When someone calls your API, the request travels through this chain:
- HTTP request arrives at your API Gateway HTTP API endpoint (e.g.
https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/items/42) - API Gateway receives the request, applies any configured auth or throttling, and converts it into a Lambda event payload
- Lambda receives the event and passes it to your handler function
- Mangum receives the Lambda event and translates it into an ASGI-format request that FastAPI understands
- FastAPI processes the request, validates inputs, runs your route handler, and builds a response
- Mangum converts the FastAPI response back into a Lambda response format, and API Gateway returns it as an HTTP response
Mangum in one analogy
Think of Mangum as a conference interpreter. API Gateway speaks Lambda event format. FastAPI speaks ASGI. Without an interpreter, they cannot communicate. Mangum listens to Lambda, converts the message into ASGI, hands it to FastAPI, then takes FastAPI’s response and converts it back. Your FastAPI app never needs to know it is running inside Lambda.
Lambda is stateless. Each invocation runs in an execution environment that may be new (cold start) or reused from a previous request (warm start). In-memory data does not persist reliably across invocations and is not shared between concurrent executions. Real application data such as user records, items, or sessions must live in an external store like DynamoDB or RDS.
When to use FastAPI on Lambda
Lambda is a strong fit when:
- Traffic is variable, bursty, or unpredictable. You pay per request, not for idle time.
- You want zero server management and automatic scaling without configuring Auto Scaling Groups
- Request handlers complete quickly, typically under a few seconds, well within the 15-minute limit
- You are building internal tools, webhooks, lightweight APIs, or microservices with moderate throughput
- You want to prototype quickly and only pay when the API is actually used
When not to use Lambda
- Long-running jobs. Lambda has a 15-minute hard limit per invocation. Use AWS Batch or Step Functions instead.
- Always-on high-throughput APIs. If your API handles thousands of requests per second around the clock, ECS or EC2 is cheaper. Per-invocation overhead adds up at sustained scale.
- Heavy dependency sets. If your app requires pandas, numpy, or large ML libraries, package size and cold starts become serious problems. Consider container images or move to App Runner.
- WebSocket or persistent connections. Lambda is request-response only. It cannot hold open connections. Use API Gateway WebSocket APIs with DynamoDB for connection state, or consider ECS.
- Ultra-low-latency on every request. Cold starts add latency. Provisioned concurrency helps but adds cost. Evaluate whether ECS Fargate gives better predictability for your SLA.
For a structured comparison of these options, see the Lambda vs ECS vs EC2 decision guide.
Architecture overview
The standard setup uses API Gateway HTTP API (v2) as the public endpoint and a single Lambda function running your entire FastAPI app:
Internet → API Gateway HTTP API → Lambda function
└── Mangum adapter
└── FastAPI app
└── Your route handlersAPI Gateway handles TLS termination, throttling, and optional auth. Lambda handles the compute. Mangum bridges Lambda’s event format and FastAPI’s ASGI interface.
API Gateway HTTP API vs Lambda Function URL
You have two options for giving your Lambda function an HTTP endpoint:
| Option | Best for | Trade-offs |
|---|---|---|
| API Gateway HTTP API | Production APIs needing auth, throttling, custom domains | Small per-request cost; more setup steps |
| Lambda Function URL | Internal tools, webhooks, dev/test, simple APIs | No built-in JWT auth or throttling; you handle auth inside FastAPI |
This guide uses API Gateway HTTP API as the main walkthrough. For simpler use cases such as a webhook handler or an internal API, a Lambda Function URL gives you a direct HTTPS endpoint with far less configuration.
Prerequisites
- AWS account with permissions to create Lambda functions, IAM roles, and API Gateway APIs
- AWS CLI installed and configured (
aws configure) - Python 3.12 installed locally
- Basic familiarity with FastAPI. This guide does not cover FastAPI fundamentals.
- An IAM execution role for Lambda with at least
AWSLambdaBasicExecutionRoleattached. See the first Lambda function guide for the full IAM setup.
Project structure
my-fastapi-lambda/
├── app/
│ ├── __init__.py
│ ├── main.py ← FastAPI application
│ └── routes/
│ └── items.py
├── lambda_handler.py ← Lambda entry point (Mangum wrapper)
├── requirements.txt
└── build.sh ← Packaging scriptKeep lambda_handler.py separate from your app code. Lambda’s handler setting points to this file (lambda_handler.handler), not to your FastAPI app directly.
Minimal FastAPI app
Write a standard FastAPI app in app/main.py. The example below uses an in-memory dict as a demo stand-in for a real data store. It is fine for local testing but not for production.
# app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
app = FastAPI(
title="My Lambda API",
description="FastAPI running on AWS Lambda",
version="1.0.0"
)
class Item(BaseModel):
name: str
price: float
description: Optional[str] = None
# Demo only: this in-memory store is lost when the execution environment is
# recycled and is NOT shared across concurrent Lambda executions.
# Replace with DynamoDB or another external store in production.
items: dict = {}
@app.get("/")
def root():
return {"message": "FastAPI on Lambda is working"}
@app.get("/health")
def health():
return {"status": "healthy"}
@app.get("/items/{item_id}")
def get_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return items[item_id]
@app.post("/items/{item_id}")
def create_item(item_id: str, item: Item):
items[item_id] = item.model_dump() # Pydantic v2: model_dump() replaces .dict()
return {"message": "Item created", "item_id": item_id}This example uses Pydantic v2. item.model_dump() replaces the deprecated item.dict() from Pydantic v1. Pydantic v2 is significantly faster at validation and startup, which helps cold start times.
Lambda handler with Mangum
Create lambda_handler.py at the project root. This is the file Lambda calls when a request arrives:
# lambda_handler.py
from mangum import Mangum
from app.main import app
# lifespan="off" disables ASGI startup/shutdown events, which Lambda does not support.
# Any initialisation logic (DB clients, config loading) should be at module level,
# outside any function, so it runs once per execution environment.
handler = Mangum(app, lifespan="off")The lifespan=“off” parameter is important. By default, Mangum tries to run ASGI lifespan events (startup/shutdown). Lambda does not support these because each invocation is independent and stateless. Setting lifespan=“off” skips them cleanly. If you use FastAPI’s @app.on_event(“startup”) hooks, move that logic to module-level code instead (outside any function definition), so it runs once when the execution environment initialises.
Dependencies and packaging
requirements.txt
Pin exact versions for deterministic builds. Use pip-tools or pip freeze to generate a locked file from your development environment:
fastapi==0.110.0
mangum==0.17.0
pydantic==2.6.0Building the ZIP package
Lambda’s ZIP deployment requires your code and all dependencies bundled together. This script installs dependencies into a staging directory and zips everything:
#!/bin/bash
# build.sh
set -e
rm -rf dist/
mkdir dist/
# Install dependencies flat into dist/
pip install -r requirements.txt -t dist/ --quiet
# Copy application code
cp -r app/ dist/
cp lambda_handler.py dist/
# Create zip, excluding compiled bytecode
cd dist
zip -r ../function.zip . -x "*.pyc" -x "*/__pycache__/*"
cd ..
echo "Build complete: function.zip ($(du -sh function.zip | cut -f1))"chmod +x build.sh && ./build.shCheck the output size. FastAPI, Mangum, and Pydantic v2 together total around 15 to 20 MB unzipped, well under Lambda’s 250 MB unzipped limit.
ZIP vs container image
ZIP packages are simpler and deploy faster. Switch to a container image (via ECR and Lambda container image support) when:
- Your unzipped dependencies exceed 250 MB, which is common when adding pandas, numpy, or scikit-learn
- You need a specific system-level dependency such as shared libraries or custom binaries
- You want a fully reproducible, version-controlled build environment
Container images support up to 10 GB, but larger images mean longer cold starts. Keep images lean by using multi-stage Docker builds and avoiding dev dependencies in the final image.
If your app only uses FastAPI, Pydantic, and a few lightweight HTTP libraries, ZIP packaging is fine. If you see yourself adding pandas, SQLAlchemy, or any library that brings in large compiled extensions, plan for container images from the start rather than trying to optimise your way out of the size limit later.
Deployment walkthrough
Step 1: Create the Lambda function
Replace the ARN with your actual IAM execution role ARN (see the first Lambda guide for role creation):
aws lambda create-function \
--function-name fastapi-app \
--runtime python3.12 \
--role arn:aws:iam::123456789012:role/MyLambdaExecutionRole \
--handler lambda_handler.handler \
--zip-file fileb://function.zip \
--timeout 30 \
--memory-size 256 \
--environment "Variables={ENVIRONMENT=production}"
aws lambda wait function-active --function-name fastapi-appStep 2: Test the function directly
Before connecting API Gateway, verify the function responds correctly by simulating an API Gateway HTTP API event payload:
aws lambda invoke \
--function-name fastapi-app \
--cli-binary-format raw-in-base64-out \
--payload '{
"version": "2.0",
"routeKey": "GET /",
"rawPath": "/",
"rawQueryString": "",
"headers": {"content-type": "application/json"},
"requestContext": {
"http": {"method": "GET", "path": "/", "protocol": "HTTP/1.1"}
},
"isBase64Encoded": false
}' \
response.json
cat response.json
# Expected: {"statusCode":200,"body":"{\"message\":\"FastAPI on Lambda is working\"}","headers":...}Step 3: Create an API Gateway HTTP API
Create the API and capture the ID. You need it for subsequent commands:
API_ID=$(aws apigatewayv2 create-api \
--name fastapi-http-api \
--protocol-type HTTP \
--query "ApiId" \
--output text)
echo "API ID: $API_ID"Step 4: Create the Lambda integration
Set payload format version 2.0. This is what Mangum expects from API Gateway HTTP API:
INTEGRATION_ID=$(aws apigatewayv2 create-integration \
--api-id $API_ID \
--integration-type AWS_PROXY \
--integration-uri arn:aws:lambda:us-east-1:123456789012:function:fastapi-app \
--payload-format-version "2.0" \
--query "IntegrationId" \
--output text)Step 5: Add routes and deploy to a stage
Add a catch-all route to forward all paths to the Lambda function, then create a prod stage with auto-deploy enabled:
# Catch-all route for all methods and sub-paths
aws apigatewayv2 create-route \
--api-id $API_ID \
--route-key "ANY /{proxy+}" \
--target "integrations/$INTEGRATION_ID"
# Root route
aws apigatewayv2 create-route \
--api-id $API_ID \
--route-key "ANY /" \
--target "integrations/$INTEGRATION_ID"
# Deploy to a stage
aws apigatewayv2 create-stage \
--api-id $API_ID \
--stage-name prod \
--auto-deployStep 6: Grant API Gateway permission to invoke Lambda
Without this resource-based policy, API Gateway will receive a 500 when it tries to call your function:
aws lambda add-permission \
--function-name fastapi-app \
--statement-id api-gateway-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-east-1:123456789012:$API_ID/*/*"Step 7: Get the public endpoint URL
aws apigatewayv2 get-api \
--api-id $API_ID \
--query "ApiEndpoint" \
--output text
# https://YOUR-API-ID.execute-api.us-east-1.amazonaws.comTesting your deployment
Test the public endpoint with curl:
# Root endpoint
curl https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/
# {"message":"FastAPI on Lambda is working"}
# Health check
curl https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/health
# {"status":"healthy"}
# Create an item (POST)
curl -X POST https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/items/widget1 \
-H "Content-Type: application/json" \
-d '{"name": "Widget", "price": 9.99}'
# {"message":"Item created","item_id":"widget1"}
# Retrieve that item
curl https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/items/widget1
# {"name":"Widget","price":9.99,"description":null}FastAPI’s auto-generated interactive docs are available at /docs. Open https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/docs in a browser to verify all routes are visible and responding. The Swagger UI is a useful sanity check that Mangum is routing requests correctly and FastAPI is parsing the payload format properly.
Production considerations
Cold starts
A minimal FastAPI app with Pydantic v2 cold-starts in roughly 500ms to 1 second. This is acceptable for many use cases. Cold starts get worse as dependencies grow because each additional library adds initialisation time. To manage cold starts:
- Keep
requirements.txtlean. Only include what your app actually uses. - Prefer Pydantic v2 over v1 (significantly faster startup and validation)
- Move heavy initialisation such as DB client creation and config loading to module level. It runs once per execution environment, not once per request.
- Use provisioned concurrency for latency-sensitive endpoints. It keeps execution environments pre-warmed and eliminates cold starts entirely. See Lambda scaling and concurrency.
Provisioned concurrency is not always worth the cost. A FastAPI Lambda receiving traffic every few minutes will already have warm environments most of the time. Only add provisioned concurrency if cold starts are actually showing up in your latency metrics, not as a precaution.
Persistence and databases
Lambda is stateless. Never rely on in-memory structures for application data. Use external storage appropriate to your data model:
- DynamoDB is the natural fit for Lambda. It is serverless, scales alongside your function, has no connection limit problems, and has no idle cost.
- RDS with RDS Proxy for relational data. Without RDS Proxy, concurrent Lambda executions each open their own database connection. At 200 concurrent executions that is 200 simultaneous connections, which exhausts most small RDS instances. RDS Proxy pools connections between Lambda and the database so this does not happen.
- ElastiCache for caching. Requires VPC access, which adds some cold start latency.
Security and auth
Before going to production, read the Lambda security model and add at least one auth layer:
- API Gateway JWT authorizer validates JWTs from Cognito or another IdP before Lambda is ever invoked
- FastAPI middleware handles auth inside your app (API key checking, OAuth2, etc.)
- IAM auth for internal AWS service-to-service calls where the caller can sign requests
The default API Gateway setup created in this guide has no authentication. Your endpoint is publicly accessible as soon as you deploy. Add a JWT authorizer or FastAPI auth middleware before sharing the URL outside your team.
Store sensitive configuration such as DB passwords, API keys, and secrets in AWS Secrets Manager or SSM Parameter Store. Do not hardcode them in environment variables or source code.
Logging and monitoring
Lambda automatically captures stdout and stderr to CloudWatch Logs. Use Python’s standard logging module rather than print statements. Structured log output is easier to filter and query. For production observability, see monitoring Lambda with CloudWatch.
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
@app.get("/items/{item_id}")
def get_item(item_id: str):
logger.info("Fetching item %s", item_id)
...Cost control
Lambda pricing is based on request count and duration in GB-seconds. FastAPI functions are typically fast, so cost is usually low. To keep it predictable:
- Right-size memory. More memory also means more CPU, which can reduce execution duration and lower total cost. Test at 128 MB, 256 MB, and 512 MB to find the sweet spot for your workload.
- Set reasonable timeouts. The default is 3 seconds. Set it to something appropriate for your app rather than the maximum 15 minutes. Stuck invocations should fail fast, not accumulate cost.
- Avoid unnecessary invocations. Health check endpoints that trigger expensive operations run up your bill quickly when polled by a load balancer every few seconds.
See Lambda cost optimisation for a deeper look at pricing and memory tuning.
Infrastructure and deployment tooling
The AWS CLI walkthrough in this guide is good for understanding how things work, but for team development you want repeatable infrastructure. Consider AWS SAM (purpose-built for Lambda), Terraform, or AWS CDK. If your app has grown beyond Lambda’s sweet spot due to large dependencies, sustained traffic, or complex connection requirements, evaluate App Runner or ECS Fargate. Both run a containerised FastAPI app without Lambda’s constraints.
Common mistakes
- Relying on in-memory state. The in-memory
itemsdict in the example is for demos only. In production, data stored in-memory is lost when the execution environment is recycled and is not shared between concurrent executions. Always use an external store like DynamoDB for application data. - Bad database connection handling. Without RDS Proxy, each concurrent Lambda execution opens its own database connection. 200 concurrent executions means 200 simultaneous connections, more than most small RDS instances support. Always use RDS Proxy for relational databases, and set connection timeout short (3 to 5 seconds) so the function fails fast rather than hanging.
- Oversized packages causing slow cold starts. Adding pandas, numpy, or ML libraries to a Lambda ZIP can push cold starts from under 1 second to several seconds and may exceed the 250 MB unzipped limit. Audit dependencies regularly. Switch to a container image when size becomes a problem.
- No authentication on the public endpoint. A freshly created API Gateway plus Lambda setup has no auth. Any public endpoint is reachable by anyone. Add a JWT authorizer on API Gateway or handle auth in FastAPI middleware before going live.
- Not understanding cold starts. Cold starts are real. They add latency on the first request to a new execution environment. They are often acceptable, but not always. Know your p99 latency requirements before choosing Lambda for a user-facing API.
- Forgetting
lifespan=“off”. If you use FastAPI startup event handlers and forget to setlifespan=“off”in Mangum, it will attempt to run unsupported lifespan events. Move startup logic to module level where it runs once when the environment initialises. - Wrong payload format version. API Gateway HTTP API v2 uses payload format version 2.0. If you configure the integration to use version 1.0 by mistake, Mangum will misparse the event. Always set
—payload-format-version “2.0”when creating the HTTP API integration. - Choosing Lambda for the wrong workload. Lambda is not a general-purpose web server. If your API needs sustained high-throughput, WebSocket connections, or sub-50ms latency on every request, evaluate ECS Fargate or App Runner instead.
FastAPI on Lambda vs other AWS options
Lambda is one of several ways to run a FastAPI app on AWS. The right choice depends on your traffic pattern, latency requirements, and how much operational overhead you want.
| Option | Best for | Key trade-offs | When to choose |
|---|---|---|---|
| Lambda + API Gateway | Variable/bursty traffic, low idle cost, microservices | Cold starts, 15-min limit, stateless execution | Traffic is unpredictable or sparse; zero server management matters |
| ECS Fargate | Sustained traffic, containerised apps, WebSockets | Always-on cost, more configuration than Lambda | You need persistent connections, predictable latency, or long-running tasks |
| EC2 | Full control, high-throughput, custom runtimes | Server management overhead, manual scaling | You need specific instance types, custom networking, or maximum cost control at scale |
| App Runner | Containerised apps without managing clusters | Higher minimum cost than Lambda at low volume | You want container deployment simplicity with always-on, predictable performance |
See the Lambda vs ECS comparison, the Lambda vs EC2 comparison, and the App Runner overview for more detail on each path.
Summary
- Mangum is the ASGI adapter that makes FastAPI work inside Lambda. Wrap your app with
handler = Mangum(app, lifespan=“off”). - Use API Gateway HTTP API (v2) with payload format version 2.0 and a catch-all
ANY /{proxy+}route. For simpler use cases, a Lambda Function URL is a lighter alternative. - ZIP packaging works for most FastAPI apps (15 to 20 MB unzipped). Switch to container images if dependencies exceed 250 MB.
- Lambda is stateless. Use DynamoDB or RDS with RDS Proxy for persistent data, never in-memory dicts.
- FastAPI cold starts run 500ms to 1 second with minimal dependencies. Use provisioned concurrency for latency-sensitive endpoints only after confirming cold starts are a real problem.
- Add authentication (JWT authorizer or FastAPI middleware) before going to production. The default setup has none.
- Use Pydantic v2 and
item.model_dump()for faster startup and validation.
Frequently asked questions
Do I need Mangum to run FastAPI on Lambda?
Yes. FastAPI is an ASGI framework that expects requests in ASGI format. Lambda receives events from API Gateway in a completely different format. Mangum translates between the two, so your FastAPI app runs inside Lambda without any changes to the app code itself.
API Gateway HTTP API vs Lambda Function URL: which should I choose?
API Gateway HTTP API gives you JWT authorizers, throttling, custom domains, and usage plans. Lambda Function URL is a simpler, cheaper option: a direct HTTPS endpoint attached to your function with no extra configuration. Use Function URL for internal tools, webhooks, or simple APIs where you handle auth yourself. Use API Gateway when you need production-grade controls.
When should I use a container image instead of a ZIP package?
Switch to a container image when your unzipped dependencies exceed Lambda's 250 MB limit. This is common when you add data science libraries like pandas, numpy, or scikit-learn. Container images support up to 10 GB and give you full control over the runtime environment.
How should I handle databases and persistent state on Lambda?
Lambda execution environments are stateless and ephemeral. In-memory data is lost when an environment is recycled and is not shared between concurrent executions. Use DynamoDB for key-value and document data, RDS with RDS Proxy for relational data (to avoid connection exhaustion), or S3 for file storage. Never rely on in-memory state for real application data.
Are cold starts a real problem for FastAPI on Lambda?
A minimal FastAPI app cold-starts in 500ms to 1 second. This is often acceptable. Cold starts become a problem when you add large libraries (pushing startup to several seconds), when your API has strict latency SLAs, or when traffic is so sparse that environments are rarely warm. Provisioned concurrency eliminates cold starts but adds cost. Use it only for latency-sensitive production endpoints.