Python API deployment options: best practices explained
You've built a Python API that works locally. Now comes the harder part: where do you actually run it in production? The landscape in 2026 is crowded with choices, each with trade-offs that matter to your ops budget, team expertise, and traffic patterns. We'll walk through the real deployment options—not the marketing claims—and show you how to pick the right one for your team.
Options — Pick by ops appetite
Before diving into specific platforms, understand that deployment boils down to: How much infrastructure do you want to manage yourself? The answer determines which tools fit.
| Platform | Best For | Ops burden | Typical cost |
|---|---|---|---|
| AWS ECS Fargate | Containers without servers | Low | $50–500/month |
| AWS Lambda | Spiky, low-traffic APIs | Very low | Pay per request |
| VPS + Docker Compose | Small projects on a budget | Medium–high | $5–50/month |
| Kubernetes | Teams already running clusters | High | $100+/month |
Each option reflects a philosophy: serverless platforms eliminate server thinking entirely. Containers offer flexibility without bare-metal pain. VPS deployments keep costs low at the expense of hands-on maintenance. Kubernetes shines when you're coordinating multiple services and need auto-scaling.
AWS ECS Fargate for containers without servers
ECS Fargate lets you run Docker containers without owning or patching EC2 instances. You define task definitions, push images to ECR, and Fargate handles the compute layer. It sits between Lambda (stateless and cold-start prone) and full Kubernetes (operationally heavy).
A typical FastAPI service on Fargate costs $30–100/month for a small production setup. You get load balancing, auto-scaling based on CPU or memory, and native AWS integrations. The trade-off: you're locked into AWS, and you still need to understand container logs, networking, and service discovery basics.
AWS Lambda for spiky, low-traffic APIs
Lambda works well when your API traffic is unpredictable or genuinely low-volume. You pay for execution time in 1ms increments; no traffic means no cost. Cold starts—the delay on first invocation—remain a real issue for Python, typically 500ms to 2 seconds depending on package size.
Lambda shines for webhooks, scheduled jobs, or internal APIs. Public-facing REST APIs with sustained traffic usually hit Lambda's cold-start ceiling unless you keep a warmer running (which defeats the cost savings).
VPS with Docker Compose for small projects on a budget
A $10–20/month VPS from Linode, DigitalOcean, or Hetzner, combined with Docker Compose, is the workhorse for bootstrapped teams and side projects. You get full control, no vendor lock-in, and predictable costs. The downside: you own patching, backups, monitoring, and scaling.
This approach scales to medium-traffic APIs if you're comfortable with Linux administration. Many mature SaaS products run on a single VPS for years before needing to upgrade.
Kubernetes for teams already running clusters
If your organization already operates Kubernetes—whether self-hosted or via EKS, GKE, or AKS—deploying your Python API there makes sense. You get declarative deployment, built-in service discovery, and orchestration. But Kubernetes has a steep learning curve and hidden costs: cluster management, persistent volumes, ingress controllers, and observability tooling.
Only choose Kubernetes if you're managing multiple services or genuinely need its auto-scaling and resilience features. A single Python API on Kubernetes is usually overkill.
Lambda with Mangum — Wrap FastAPI for Lambda
If you've chosen Lambda, adapting your FastAPI application requires a bridge. Mangum is an ASGI adapter that translates Lambda's event-driven model into ASGI, letting your existing FastAPI code run without major refactoring.
Here's a typical setup:
from fastapi import FastAPI
from mangum import Mangum
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id, "name": "Alice"}
# Expose the ASGI app to Lambda
handler = Mangum(app)Your handler is the Lambda handler function. AWS invokes it on each request. Mangum unpacks the Lambda event, calls your FastAPI app, and returns a Lambda-compatible response.
For production, keep your Lambda deployment package under 50MB (uncompressed). Use Lambda layers for shared dependencies to speed up deployments:
# Directory structure
lambda-deployment/
├── src/
│ ├── main.py # Your FastAPI app
│ └── handler.py # Mangum wrapper
├── requirements.txt
└── .github/workflows/deploy.yml # CI/CD (see below)Lambda cold starts are manageable for internal APIs or webhooks. For customer-facing APIs with sustained traffic, consider Fargate instead. If you do choose Lambda, enable Provisioned Concurrency (costs money but eliminates cold starts), or add a CloudWatch Events rule to keep the function warm with periodic invocations.
Managed databases — RDS for Postgres with automated backups
Your Python API almost certainly needs a database. Running PostgreSQL on the same VPS or container works for small projects, but production workloads belong on managed services. AWS RDS, Google Cloud SQL, or Azure Database for PostgreSQL handle patching, backups, failover, and point-in-time recovery.
A db.t3.micro RDS Postgres instance costs roughly $15–40/month depending on region and multi-AZ redundancy. You get automated daily snapshots, automated minor patching, and the ability to restore to any point in time (within your backup window).
From your Python code, the connection looks identical to any Postgres setup. Use SQLAlchemy or an async driver like asyncpg:
import asyncpg
# RDS endpoint from AWS console
DB_URL = "postgresql://user:password@my-db.c9akciq32.us-east-1.rds.amazonaws.com/myapp"
async def get_pool():
return await asyncpg.create_pool(DB_URL, min_size=5, max_size=20)
async def fetch_user(pool, user_id):
async with pool.acquire() as conn:
row = await conn.fetchrow("SELECT * FROM users WHERE id = $1", user_id)
return dict(row) if row else NoneEnable encryption at rest, use VPC security groups to restrict access to your application tier only, and rotate credentials regularly (or use AWS Secrets Manager for automatic rotation).
CI/CD — GitHub Actions to build, test and deploy on every merge
Manual deployments are a source of inconsistency and human error. Automate the full pipeline: test, build, push to a registry, and deploy to your target platform on every merge to main.
A typical GitHub Actions workflow for Fargate:
name: Deploy to Fargate
on:
push:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: my-api
ECS_SERVICE: my-api-service
ECS_CLUSTER: production
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: pytest tests/
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Update ECS service
run: |
aws ecs update-service \
--cluster $ECS_CLUSTER \
--service $ECS_SERVICE \
--force-new-deploymentThis workflow runs tests before building, prevents broken code from reaching production, and uses short-lived AWS credentials. For Lambda, replace the ECS update step with:
- name: Deploy to Lambda
run: |
aws lambda update-function-code \
--function-name my-api \
--s3-bucket my-deployments \
--s3-key lambda-${{ github.sha }}.zipSecrets (AWS keys, database passwords) should never be hardcoded. Use GitHub Secrets to inject them at runtime. Commit a .env.example file with placeholder values so team members know what's required.
Common mistakes
- Deploying to Lambda without cold-start mitigation. If your API is synchronous and user-facing, Lambda may cause visible delays. Test latency before committing to serverless.
- Skipping database backups. Using RDS but not enabling automated backups is like having no insurance. Enable multi-AZ and automated snapshots, even in development, to establish good habits.
- Hardcoding secrets in code or Dockerfiles. Environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault) are non-negotiable. Rotate credentials every 90 days.
- Running tests locally but skipping CI. Your CI pipeline should run the same tests, in the same order, on every merge. Differences between local and CI environments breed production bugs.
- Ignoring observability from day one. Deploy structured logging (JSON format), application metrics (request latency, error rates), and traces before you need them in an outage. Use CloudWatch, DataDog, or New Relic.
- Choosing a platform before understanding traffic patterns. If you don't know whether your API will see 10 requests/day or 10,000/day, pick the most flexible option (VPS or Fargate) until you do. Migrate later.
Key takeaways
- Fargate is the safe middle ground for most Python APIs: low ops burden, no cold starts, easy to scale.
- Lambda works for webhooks and low-traffic internal APIs; use Mangum to adapt FastAPI without rewriting.
- VPS + Docker Compose keeps costs minimal for bootstrapped teams; acceptable ops burden if you're comfortable with Linux.
- Kubernetes is worth it only if you're already running multiple services; a single Python API doesn't justify the complexity.
- Always use managed databases (RDS, Cloud SQL) in production; point-in-time recovery and automated backups are table stakes.
- Automate testing and deployment with CI/CD; manual deployments are slow and error-prone.
- Store secrets in a secrets manager, never in code or environment variables passed directly in Dockerfiles.
- Measure your API's actual traffic and latency before optimizing; assumptions often lead to the wrong technology choice.






Comments
Loading comments…