Skip to content
ExponaBLOG
PYTHON BACKEND DEVELOPMENT · DAY 066 / 100

Python API deployment options: best practices explained

Where to run Python APIs in 2026.

Expona Admin7 min read

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:

python
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:

python
# 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:

python
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 None

Enable 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:

python
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-deployment

This 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:

python
- name: Deploy to Lambda
  run: |
    aws lambda update-function-code \
      --function-name my-api \
      --s3-bucket my-deployments \
      --s3-key lambda-${{ github.sha }}.zip

Secrets (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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles