AWS API Gateway: REST and HTTP APIs Complete Guide (2026)

AWS API Gateway is a fully managed service for creating, publishing, and securing APIs at any scale. It handles traffic management, authorization, throttling, monitoring, and versioning so your backend code doesn't have to. There are three flavors: REST APIs (the original, feature-rich option), HTTP APIs (newer, faster, cheaper), and WebSocket APIs (for real-time bidirectional communication). Choosing the right one — and configuring it correctly — makes a significant difference in cost, latency, and maintainability.

REST API vs HTTP API vs WebSocket API

Pick the wrong API type and you'll either overpay or hit feature gaps. Here's the decision matrix:

  • FeatureREST APIHTTP APIWebSocket API
    Latency~6ms overhead~1ms overheadPersistent
    Price per million calls$3.50$1.00$1.00 + connection fees
    Request/response transformsYes (VTL templates)NoNo
    Usage plans / API keysYesNo (use JWT claims)No
    Edge-optimized (CloudFront)YesNoNo
    WAF integrationYesYesNo
    Private integrations (VPC Link)Yes (NLB)Yes (ALB/NLB/Cloud Map)No
    Best forComplex API management, B2B APIs with API keysSimple Lambda-backed APIs, microservicesChat, live dashboards, gaming
    Default recommendation: Start with HTTP API. It's 70% cheaper than REST API and covers 90% of use cases. Switch to REST API only when you need request transformation, edge optimization, or API key-based usage plans.

    Lambda Proxy Integration

    Lambda proxy integration is the simplest way to connect API Gateway to Lambda. The entire HTTP request (headers, query params, body, path variables) is passed to Lambda as a structured JSON event. Your Lambda returns a JSON object with statusCode, headers, and body.

    # Lambda handler for a REST API proxy integration (Python)
    import json
    
    def handler(event, context):
        # event contains: httpMethod, path, pathParameters,
        # queryStringParameters, headers, body, requestContext
    
        method = event['httpMethod']
        path = event['path']
        user_id = event.get('pathParameters', {}).get('userId')
        query = event.get('queryStringParameters') or {}
    
        if method == 'GET' and user_id:
            user = get_user_from_db(user_id)
            if not user:
                return {
                    'statusCode': 404,
                    'headers': {'Content-Type': 'application/json'},
                    'body': json.dumps({'error': 'User not found'})
                }
            return {
                'statusCode': 200,
                'headers': {
                    'Content-Type': 'application/json',
                    'Cache-Control': 'max-age=60'
                },
                'body': json.dumps(user)
            }
    
        return {
            'statusCode': 405,
            'body': json.dumps({'error': 'Method not allowed'})
        }

    For HTTP APIs, the event format is slightly different (v2 payload format). The body is a plain string, and the method/path come from event['requestContext']['http'].

    # HTTP API v2 payload format
    def handler(event, context):
        http_context = event['requestContext']['http']
        method = http_context['method']
        path = http_context['path']
        body = json.loads(event.get('body', '{}'))
    
        return {
            'statusCode': 200,
            'body': json.dumps({'message': 'OK'})
            # No need to set Content-Type — API Gateway infers it
        }

    Request/Response Mapping Templates

    REST API mapping templates use Velocity Template Language (VTL) to transform requests before they hit the backend and transform responses before returning to the client. This lets you adapt a legacy backend to a modern API contract without changing the backend code.

    ## Request mapping template: extract JSON body fields and pass as query string
    ## (useful for SQS SendMessage integration)
    Action=SendMessage
    &QueueUrl=$util.urlEncode("https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue")
    &MessageBody=$util.urlEncode($input.body)
    &MessageAttribute.1.Name=eventType
    &MessageAttribute.1.Value.DataType=String
    &MessageAttribute.1.Value.StringValue=$util.urlEncode($input.path('$.eventType'))
    ## Response mapping template: reshape the backend response
    #set($inputRoot = $input.path('$'))
    {
      "id": "$inputRoot.SendMessageResponse.SendMessageResult.MessageId",
      "status": "queued"
    }

    This pattern — API Gateway → SQS (no Lambda) — is a cost-effective way to ingest webhooks. You pay only for API Gateway and SQS calls, no Lambda cold starts on the ingestion path.

    Authorizers: Lambda and Cognito

    API Gateway supports three authorization mechanisms: IAM (for AWS service-to-service), Cognito User Pools (for user authentication), and Lambda authorizers (custom logic).

    Cognito authorizer — validates a JWT from Cognito User Pools:

    # Attach a Cognito authorizer to an HTTP API
    aws apigatewayv2 create-authorizer \
      --api-id abc123 \
      --authorizer-type JWT \
      --identity-source '$request.header.Authorization' \
      --name CognitoAuth \
      --jwt-configuration '{
        "Audience": ["your-app-client-id"],
        "Issuer": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID"
      }'

    Lambda authorizer — for custom token validation (API keys, internal JWTs, OAuth introspection):

    import json
    
    def handler(event, context):
        token = event.get('authorizationToken', '').replace('Bearer ', '')
    
        # Validate the token against your auth service
        principal_id = validate_token(token)
        if not principal_id:
            raise Exception('Unauthorized')
    
        return {
            'principalId': principal_id,
            'policyDocument': {
                'Version': '2012-10-17',
                'Statement': [{
                    'Action': 'execute-api:Invoke',
                    'Effect': 'Allow',
                    'Resource': event['methodArn']
                }]
            },
            'context': {
                'userId': principal_id,
                'tier': 'premium'  # passed to Lambda backend via $context.authorizer.tier
            }
        }
    Cache your Lambda authorizer results. Set the TTL to 300 seconds (5 minutes) in the authorizer config. API Gateway caches the policy by token, so you pay for one Lambda invocation per token per 5 minutes instead of one per request.

    Throttling and Usage Plans

    API Gateway throttles at two levels: account-level defaults (10,000 RPS burst, 5,000 RPS steady-state per region) and per-stage/method limits. Usage plans let you give different API key holders different rate limits.

    # Create a usage plan with per-key throttling
    aws apigateway create-usage-plan \
      --name "Standard" \
      --api-stages '[{"apiId":"abc123","stage":"prod"}]' \
      --throttle '{"burstLimit":100,"rateLimit":50}' \
      --quota '{"limit":10000,"period":"MONTH"}'
    
    # Create an API key and associate with the plan
    KEY_ID=$(aws apigateway create-api-key \
      --name "partner-acme" --enabled \
      --query 'id' --output text)
    
    aws apigateway create-usage-plan-key \
      --usage-plan-id plan123 \
      --key-id $KEY_ID \
      --key-type API_KEY

    When a client exceeds their quota, they receive HTTP 429 Too Many Requests. Implement exponential backoff in your API clients to handle this gracefully.

    Response Caching

    REST APIs support response caching at the stage level (0.5 GB to 237 GB). Cached responses bypass your backend entirely, reducing latency and cost for read-heavy endpoints.

    # Enable caching on a specific method
    aws apigateway update-stage \
      --rest-api-id abc123 \
      --stage-name prod \
      --patch-operations \
        op=replace,path=/cacheClusterEnabled,value=true \
        op=replace,path=/cacheClusterSize,value=0.5 \
        op=replace,path=/~1users~1{userId}/GET/caching/enabled,value=true \
        op=replace,path=/~1users~1{userId}/GET/caching/ttlInSeconds,value=60
    Note: Caching is billed per hour for the cache size (even if unused). A 0.5 GB cache costs ~$0.02/hour (~$14/month). Only enable it on endpoints with high read traffic and stable responses. HTTP APIs do not support response caching — use CloudFront in front of HTTP APIs instead.

    CORS Configuration

    CORS errors are the most common API Gateway gotcha for frontend developers. For HTTP APIs, CORS is configured at the API level. For REST APIs, you must configure CORS per-resource and ensure your Lambda returns the correct headers.

    # HTTP API: Configure CORS at the API level (cleanest approach)
    aws apigatewayv2 update-api \
      --api-id abc123 \
      --cors-configuration '{
        "AllowOrigins": ["https://app.example.com"],
        "AllowMethods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
        "AllowHeaders": ["Content-Type", "Authorization", "X-Api-Key"],
        "ExposeHeaders": ["X-Request-Id"],
        "MaxAge": 3600,
        "AllowCredentials": true
      }'

    For REST APIs, also ensure your Lambda returns CORS headers in every response (not just OPTIONS), and that the OPTIONS method has a mock integration returning 200 with the appropriate headers.

    Custom Domain Names

    By default your API is at https://abc123.execute-api.us-east-1.amazonaws.com/prod. Custom domains map it to https://api.example.com.

    # Prerequisite: ACM certificate in us-east-1 (for edge-optimized) or same region (for regional)
    CERT_ARN="arn:aws:acm:us-east-1:123456789012:certificate/abc"
    
    # Create a custom domain
    aws apigateway create-domain-name \
      --domain-name api.example.com \
      --regional-certificate-arn $CERT_ARN \
      --endpoint-configuration types=REGIONAL
    
    # Map the domain to a stage
    aws apigateway create-base-path-mapping \
      --domain-name api.example.com \
      --rest-api-id abc123 \
      --stage prod \
      --base-path ""
    
    # Get the regional domain name to create a Route 53 ALIAS record
    aws apigateway get-domain-name \
      --domain-name api.example.com \
      --query 'regionalDomainName'

    Stages and Deployments

    REST APIs require explicit deployments — changes to your API definition don't go live until you deploy them to a stage. HTTP APIs auto-deploy by default (configurable).

    # Deploy a REST API to the prod stage
    aws apigateway create-deployment \
      --rest-api-id abc123 \
      --stage-name prod \
      --description "Release 2.3.0 — add user preferences endpoint"
    
    # Canary deployment: send 10% of traffic to new deployment
    aws apigateway update-stage \
      --rest-api-id abc123 \
      --stage-name prod \
      --patch-operations \
        op=replace,path=/canarySettings/percentTraffic,value=10 \
        op=replace,path=/canarySettings/deploymentId,value=NEW_DEPLOY_ID
    
    # After validation, promote canary to 100%
    aws apigateway update-stage \
      --rest-api-id abc123 \
      --stage-name prod \
      --patch-operations op=replace,path=/deploymentId,value=NEW_DEPLOY_ID

    Frequently Asked Questions

    Q: What is the maximum payload size for API Gateway?

    10 MB for REST APIs and HTTP APIs. For file uploads larger than 10 MB, generate a presigned S3 URL in your Lambda and have the client upload directly to S3. This bypasses API Gateway entirely for the binary upload.

    Q: What is the maximum integration timeout?

    29 seconds for REST and HTTP APIs (this is a hard limit, not configurable). If your Lambda takes longer than 29 seconds, the client gets a 504 Gateway Timeout. For long-running jobs, use an async pattern: API Gateway → SQS → Lambda, and poll for results separately.

    Q: How do I expose a private VPC service via API Gateway?

    Use a VPC Link. For HTTP APIs, create a VPC Link that points to an ALB, NLB, or Cloud Map service inside your VPC. API Gateway routes requests through the VPC Link without traversing the public internet. See the ECS guide for ALB setup.

    Q: How do I debug API Gateway 502 errors?

    A 502 from API Gateway means the backend returned a malformed response. For Lambda integrations, this usually means your function threw an unhandled exception or returned an object that doesn't match the expected format (missing statusCode). Enable CloudWatch execution logs on the stage at INFO level — API Gateway logs the exact error from the integration response.

    Q: Should I use API Gateway or an ALB to front my ECS service?

    ALB is cheaper (no per-request charge beyond the LCU model) and has lower latency for high-throughput services. Use API Gateway when you need: auth (JWT/Lambda authorizers), throttling, usage plans, request transformation, or a managed API catalog. For internal microservices, ALB is usually the right choice.