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.

Benefits
  • 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
Challenges
  • 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.

How It Works:
  1. API Gateway receives HTTP request from client
  2. API Gateway invokes Lambda function with event payload
  3. Mangum converts Lambda event → ASGI request object
  4. FastAPI processes request normally (routes, dependencies, validation)
  5. Mangum converts ASGI response → Lambda response format
  6. 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 format

That'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.Arn
Deploy Commands:
sam build - Build application and dependencies
sam deploy --guided - Deploy to AWS (first time, creates config)
sam deploy - Deploy updates
sam logs -n ApiFunction --tail - View CloudWatch logs in real-time
sam delete - Remove all resources

AWS Lambda Pricing

Pricing Model:
  • 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-framework
functions-framework --target=hello --debug

Runs 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-framework

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

Pricing Model:
  • 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.

requirements.txt:azure-functionsLocal Development:func start

Runs 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 TestClient

Deploying 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
Hosting Plans:
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)

Pricing Model:
  • 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

❄️ Cold Start (1-5 seconds)
  • Download code from storage
  • Start runtime environment
  • Import all dependencies
  • Initialize connections
  • Execute handler function
🔥 Warm Start (<10ms)
  • Container already running
  • Dependencies already loaded
  • Connections established
  • Execute handler only
  • Near-instantaneous response
Typical Cold Start Times:
  • 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

Reduce Package Size

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(...)
Use Lambda Layers

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:...
Provisioned Concurrency

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/month
Warming with Pings

Periodically 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...
Increase Memory

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!
Connection Pooling

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 calls

When 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?

Benefits
  • 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.
Trade-offs
  • 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: false

This 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"
Workflow:
1. serverless deploy - Packages code, creates CloudFormation stack
2. 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 done

Multi-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.

AWSprovider:
  name: aws
  runtime: python3.11
Google Cloudprovider:
  name: google
  runtime: python311
Azureprovider:
  name: azure
  runtime: python

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

FeatureAWS LambdaGoogle Cloud FunctionsAzure Functions
Max Timeout15 minutes60 minutes (gen2), 9 min (gen1)10 minutes (Consumption)
Max Memory10 GB32 GB (gen2), 8 GB (gen1)1.5 GB (Consumption), 14 GB (Premium)
Package Size50 MB zipped, 250 MB unzipped100 MB (gen2), 500 MB with Cloud Storage1 GB (compressed)
Concurrency1,000 (soft limit, can increase)1,000 (can increase)200 per app (Consumption)
Cold Start1-3s (Python), 200-500ms (Node)1-2s (Python), similar to AWS1-3s (Python), can be higher
Warm InstancesProvisioned Concurrency (~$0.015/hr)Min instances (gen2)Premium plan (always-on)
Free Tier1M requests + 400K GB-sec/month2M requests + 400K GB-sec/month1M requests + 400K GB-sec/month
Pricing (Requests)$0.20 per 1M$0.40 per 1M$0.20 per 1M
Python Runtimes3.9, 3.10, 3.11, 3.12, 3.133.10, 3.11, 3.12, 3.133.9, 3.10, 3.11, 3.12
Local TestingSAM CLI, LocalStackFunctions FrameworkAzure Functions Core Tools
IaC SupportSAM, Serverless Framework, TerraformServerless Framework, TerraformARM 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
Strengths:
  • 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
Strengths:
  • 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
Strengths:
  • Premium plan (no cold starts)
  • Durable Functions (stateful)
  • Excellent VS Code tooling
  • Flexible hosting plans

Cost Comparison: 10M Requests/Month

ScenarioAWS LambdaGoogle Cloud FunctionsAzure 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.