AWS API Gateway
Fully managed API layer. Routes HTTP requests to Lambda, ECS, EC2, or any HTTP backend.
Fully managed API layer. Routes HTTP requests to Lambda, ECS, EC2, or any HTTP backend. Three flavours: REST API (feature-rich, expensive), HTTP API (80% cheaper, modern), WebSocket API (persistent connections).
REST API vs HTTP API
| REST API | HTTP API | |
|---|---|---|
| Cost | $3.50/million | $1.00/million |
| Latency | ~6ms | ~1ms |
| Auth | IAM, Cognito, Lambda authoriser, API keys | IAM, JWT, Lambda authoriser |
| Request validation | Built-in | Manual |
| Caching | Yes | No |
| Usage plans + throttling | Yes | Basic |
| WebSocket | No | No |
Choose HTTP API for new projects unless you specifically need request validation, caching, or usage plans.
HTTP API with Lambda Integration
# Create HTTP API
API_ID=$(aws apigatewayv2 create-api \
--name my-api \
--protocol-type HTTP \
--cors-configuration AllowOrigins='["https://myapp.com"]',AllowMethods='["GET","POST"]',AllowHeaders='["Content-Type","Authorization"]' \
--query 'ApiId' --output text)
# Lambda integration
INTEGRATION_ID=$(aws apigatewayv2 create-integration \
--api-id $API_ID \
--integration-type AWS_PROXY \
--integration-uri arn:aws:lambda:eu-west-1:123456789:function:my-api \
--payload-format-version 2.0 \
--query 'IntegrationId' --output text)
# Route
aws apigatewayv2 create-route \
--api-id $API_ID \
--route-key 'ANY /{proxy+}' \
--target "integrations/$INTEGRATION_ID"
# Deploy
aws apigatewayv2 create-stage \
--api-id $API_ID \
--stage-name production \
--auto-deployLambda Handler — HTTP API v2 Payload Format
def handler(event, context):
# HTTP API v2 event structure
method = event["requestContext"]["http"]["method"]
path = event["requestContext"]["http"]["path"]
query = event.get("queryStringParameters", {}) or {}
headers = event.get("headers", {})
body = event.get("body", "")
# Auth from JWT authoriser
claims = event["requestContext"].get("authorizer", {}).get("jwt", {}).get("claims", {})
user_id = claims.get("sub")
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"userId": user_id, "path": path})
}JWT Authoriser
Validates JWT tokens from Cognito, Auth0, or any OIDC provider. Without Lambda code.
aws apigatewayv2 create-authorizer \
--api-id $API_ID \
--authorizer-type JWT \
--identity-source '$request.header.Authorization' \
--name jwt-authoriser \
--jwt-configuration \
Audience=["my-client-id"],\
Issuer="https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_XXXXX"Custom Domain
# Create custom domain
aws apigatewayv2 create-domain-name \
--domain-name api.example.com \
--domain-name-configurations \
CertificateArn=arn:aws:acm:eu-west-1:123456789:certificate/xxx,EndpointType=REGIONAL
# Map to API stage
aws apigatewayv2 create-api-mapping \
--domain-name api.example.com \
--api-id $API_ID \
--stage productionPoint your DNS CNAME to the ApiGatewayDomainName from the domain creation output.
Throttling and Usage Plans (REST API)
# Create usage plan: 10,000 req/day, 100 req/second burst
aws apigateway create-usage-plan \
--name standard-plan \
--throttle burstLimit=100,rateLimit=50 \
--quota limit=10000,period=DAY
# Create API key and link to plan
KEY_ID=$(aws apigateway create-api-key --name my-key --enabled --query 'id' --output text)
aws apigateway create-usage-plan-key --usage-plan-id $PLAN_ID --key-id $KEY_ID --key-type API_KEYWebSocket API
Persistent bidirectional connections. Lambda handles $connect, $disconnect, and $default (message) routes.
# WebSocket Lambda handler
import boto3
def handler(event, context):
route = event["requestContext"]["routeKey"]
connection_id = event["requestContext"]["connectionId"]
domain = event["requestContext"]["domainName"]
stage = event["requestContext"]["stage"]
if route == "$connect":
# Store connection_id in DynamoDB
save_connection(connection_id)
elif route == "$disconnect":
remove_connection(connection_id)
elif route == "$default":
# Send message back to client
apigw = boto3.client("apigatewaymanagementapi",
endpoint_url=f"https://{domain}/{stage}")
apigw.post_to_connection(
ConnectionId=connection_id,
Data=json.dumps({"message": "received"}).encode()
)
return {"statusCode": 200}Common Failure Cases
Lambda returns 502 Bad Gateway from API Gateway
Why: the Lambda function returned a response body that API Gateway cannot parse — typically a raw string instead of the expected {"statusCode": ..., "body": ...} format.
Detect: CloudWatch Logs for the Lambda show a successful execution, but the caller receives 502 with {"message": "Internal Server Error"}.
Fix: ensure the handler always returns a dict with statusCode, headers, and body (as a string); never return a bare Python object.
CORS preflight returns 403 even though CORS is configured
Why: CORS on HTTP API is set at the API level, but the Lambda returns its own Access-Control-Allow-Origin header, and the two conflict; or the OPTIONS route is not correctly handled.
Detect: browser console shows CORS error on the preflight OPTIONS request; the Access-Control-Allow-Origin header is missing or duplicated in the response.
Fix: either let API Gateway handle CORS entirely (remove headers from Lambda) or disable API Gateway CORS config and handle it fully inside the Lambda — never mix both.
JWT authoriser rejects valid tokens with 401
Why: the Issuer URL in the authoriser config doesn't exactly match the iss claim in the JWT (trailing slash, HTTP vs HTTPS, or wrong region in the Cognito URL).
Detect: 401 Unauthorized with {"message": "Unauthorized"} from API Gateway even though the token is freshly issued.
Fix: compare the exact string of the iss claim from jwt.io against the Issuer value configured on the authoriser; they must match character-for-character.
Custom domain not resolving after API mapping
Why: the DNS CNAME record points to the wrong value, or the ACM certificate is in the wrong region (HTTP API custom domains require a regional ACM certificate in the same region as the API).
Detect: nslookup api.example.com returns NXDOMAIN or points to a different endpoint than the ApiGatewayDomainName output.
Fix: create the ACM certificate in the same region as the API, verify the certificate is ISSUED, and update the CNAME to the exact ApiGatewayDomainName value from the domain creation response.
Connections
cloud-hub · cloud/aws-core · cloud/aws-lambda-patterns · cloud/secrets-management · cloud/cloud-monitoring
Open Questions
- What monitoring and alerting matter most when this is deployed in production?
- At what scale or workload does this approach hit its practical limits?
Related reading