API Gateway

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Commands and behaviour on this page are current for AWS CLI v2 (2.37 line) and the Amazon API Gateway documentation, all checked on 2026-10-01; re-verify against aws --version and the linked reference pages before relying on them in a pipeline. Examples use us-east-1, the placeholder account ID 111122223333, the placeholder domain example.com and shell variables such as $API_ID instead of real identifiers. Prices are never stated here; see Pricing model.

Bookshelf needs one public front door. The static site lives on S3 and CloudFront (CloudFront and CDN Delivery), but the browser also calls the back end: catalog-api and orders-api run on ECS behind the private Application Load Balancer alb-bookshelf (ECS and Fargate), and a few small endpoints are Lambda functions (AWS Lambda). Amazon API Gateway is the managed service that accepts those HTTPS requests, authenticates and throttles them, and forwards them to the right back end, so the services behind it never see anonymous or excessive traffic. This page builds the HTTP API bookshelf-api in front of a Lambda function and, through a VPC link, in front of the ALB; shows what a REST API adds and imports one from an OpenAPI file; and covers authorizers, traffic control, custom domains, operations, limits and cost. The architectural reasoning behind a gateway or a Backend for Frontend (when to have one, what belongs in it) is on API Gateway and Backends for Frontends; this page is the AWS realization. The Azure counterpart is API Management. Cloud-neutral comparisons of managed API and secret managers are tracked in issue #184 and will not be repeated here.

API types: HTTP, REST and WebSocket

API Gateway offers three API types, and the first decision is which one to create, because the type cannot be changed afterwards.

  • HTTP APIs are the lean, low-cost option: routes, Lambda and HTTP/private integrations, JWT, Lambda and IAM authorizers, CORS, automatic deployments. They are the default for new JSON APIs.

  • REST APIs are the full-featured option: API keys and usage plans, per-client throttling, request validation, AWS WAF, caching, canary releases, private endpoints, resource policies, mapping templates, execution logs, X-Ray and response streaming. They cost more per request.

  • WebSocket APIs keep a persistent two-way connection and route JSON messages to integrations by a route selection expression; they are for chat, live dashboards and notifications.

Feature HTTP API REST API WebSocket API

Endpoint types

Regional

Edge-optimized, Regional, Private

Regional

Authorization

JWT, Lambda, IAM (Cognito through a JWT authorizer)

IAM, Cognito user pool, Lambda, resource policies (JWT through a Lambda authorizer)

IAM, Lambda authorizer on $connect

AWS WAF

No (put CloudFront with WAF in front)

Yes

No

API keys, usage plans, per-client throttling

No

Yes

No

Caching

No

Yes

No

Request validation, body transformation

No (parameter mapping only)

Yes

Model-based validation on routes

Canary release deployments

No (use stages, or routing rules on a custom domain)

Yes

No

Mutual TLS on custom domains

Yes

Yes

Not covered here

Response streaming

No

Yes (proxy integrations)

Not applicable (bidirectional)

Deployment model

Optional automatic deployments per stage

Explicit deployments to stages

Explicit deployments to stages

Private integrations (VPC link)

ALB, NLB, AWS Cloud Map

ALB and NLB

NLB through a VPC link

Sources for the table are the guide Choose between REST APIs and HTTP APIs and the WebSocket developer guide; features change, so treat the table as a starting point. Practical rule for Bookshelf: use an HTTP API unless you need API keys with per-client quotas (a partner-facing API), WAF directly on the API, caching, request validation at the edge of the service or a private-only API; in those cases use a REST API. Older tutorials treat REST APIs as the only product and add API Gateway "proxy" resources and mapping templates by default; the current approach for most JSON back ends is an HTTP API with a $default or ANY proxy route and validation in the application.

How a request flows

Every API type follows the same pipeline. The ordering matters when you debug, because each stage can reject the request with its own status code.

flowchart LR C["Client"] --> D["Custom domain api.example.com
TLS, ACM certificate"] D --> M["API mapping or routing rule
picks API and stage"] M --> S["Stage
throttling, logging, variables"] S --> R["Route match
GET /books, ANY /catalog/proxy"] R --> A{"Authorizer
JWT, Lambda, IAM, Cognito"} A -- "denied: 401 or 403" --> X["Error returned
to client"] A -- "allowed" --> I["Integration
Lambda, HTTP, VPC link"] I --> B["Back end
Lambda or ALB to ECS"] B --> I I --> C

In words: the client resolves the custom domain (or the default execute-api endpoint), API Gateway terminates TLS, selects the API and stage (through an API mapping or a routing rule), applies stage-level throttling, matches the route (HTTP method plus path), runs the route’s authorizer, and only then calls the integration. A throttled request fails before the authorizer with 429; an unmatched route returns 404; an authorizer denial returns 401 (no or invalid credentials) or 403 (valid credentials, not permitted); a back end that fails or is too slow returns 500, 502, 503 or 504.

Concepts for HTTP APIs

  • API: the container, with a ProtocolType of HTTP or WEBSOCKET. Its default endpoint is https://<api-id>.execute-api.<region>.amazonaws.com.

  • Route: a route key such as GET /books, POST /orders, ANY /catalog/{proxy+} (a greedy path variable that matches the rest of the path) or the catch-all $default. The most specific route wins.

  • Integration: what a route calls. Types are AWS_PROXY (Lambda and other AWS service actions), HTTP_PROXY (a public URL, or a private ALB/NLB/Cloud Map target through a VPC link). MOCK integrations exist only on REST APIs.

  • Stage: a named, deployed snapshot such as prod, with its own throttling, logging and variables. The special $default stage serves the root path with no stage prefix in the URL. A stage with auto-deploy enabled publishes route and integration changes immediately.

  • Authorizer: JWT, Lambda (REQUEST) or IAM, attached per route.

  • Payload format version: for Lambda proxy integrations, 2.0 (the simpler event with requestContext.http, cookies and automatic response shaping) or 1.0 (the REST API event). Use 2.0 for new HTTP APIs.

Prerequisites for Bookshelf

export AWS_REGION=us-east-1
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# A throwaway Lambda used for the first API (see lambda.adoc for creating functions)
export FN_NAME=bookshelf-ping
export FN_ARN=$(aws lambda get-function --function-name "$FN_NAME" \
  --query Configuration.FunctionArn --output text)

# The private ALB that fronts catalog-api and orders-api on ECS
export ALB_ARN=$(aws elbv2 describe-load-balancers --names alb-bookshelf \
  --query 'LoadBalancers[0].LoadBalancerArn' --output text)
export LISTENER_ARN=$(aws elbv2 describe-listeners --load-balancer-arn "$ALB_ARN" \
  --query 'Listeners[?Port==`443`].ListenerArn | [0]' --output text)

Derived from the aws lambda get-function and aws elbv2 describe-listeners reference pages. The function bookshelf-ping is any Lambda that returns a small JSON body; create one from AWS Lambda. The ALB and its listener come from ECS and Fargate and Load Balancers and Gateways.

Permissions to build and run the API

The identity that creates APIs needs the apigateway:* actions on the API Gateway management ARNs (these have no account ID and no execute-api prefix) and a few neighbours. A least-privilege policy for the Bookshelf deployer, to be narrowed further per environment:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ManageBookshelfApis",
      "Effect": "Allow",
      "Action": ["apigateway:GET", "apigateway:POST", "apigateway:PUT", "apigateway:PATCH", "apigateway:DELETE"],
      "Resource": [
        "arn:aws:apigateway:us-east-1::/apis",
        "arn:aws:apigateway:us-east-1::/apis/*",
        "arn:aws:apigateway:us-east-1::/vpclinks",
        "arn:aws:apigateway:us-east-1::/vpclinks/*",
        "arn:aws:apigateway:us-east-1::/domainnames",
        "arn:aws:apigateway:us-east-1::/domainnames/*",
        "arn:aws:apigateway:us-east-1::/tags/*"
      ]
    },
    {
      "Sid": "AllowApiToInvokeLambda",
      "Effect": "Allow",
      "Action": ["lambda:AddPermission", "lambda:RemovePermission", "lambda:GetPolicy", "lambda:GetFunction"],
      "Resource": "arn:aws:lambda:us-east-1:111122223333:function:bookshelf-*"
    },
    {
      "Sid": "AccessLogGroups",
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:DescribeLogGroups", "logs:PutRetentionPolicy", "logs:DeleteLogGroup"],
      "Resource": "arn:aws:logs:us-east-1:111122223333:log-group:/aws/apigateway/bookshelf-*"
    },
    {
      "Sid": "ReadNetworkingForVpcLink",
      "Effect": "Allow",
      "Action": ["ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "ec2:DescribeVpcs",
                 "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeListeners"],
      "Resource": "*"
    }
  ]
}

Turning on access logging additionally needs the CloudWatch Logs delivery actions (logs:CreateLogDelivery, logs:PutResourcePolicy and friends) listed in Configure logging for HTTP APIs; they do not support resource-level restriction. Creating a VPC link may also need EC2 network-interface permissions or a service-linked role the first time in an account; if creation fails with an authorization error, compare the denied action against the VPC link page linked in the references. The invoke permissions of callers are separate and are covered in IAM (SigV4) authorization. To grant a CI pipeline these rights without long-lived keys, see Identity and Access.

Creating an HTTP API in front of Lambda

In the AWS Management Console

  1. Choose the Region in the selector (us-east-1) and open API Gateway.

  2. Under HTTP API choose Build, then Add integration, pick Lambda, select the function bookshelf-ping and name the API bookshelf-api.

  3. Choose Next. In Configure routes keep the suggested route (ANY /bookshelf-ping) or edit it to GET /ping.

  4. In Configure stages keep the $default stage with Auto-deploy on, choose Next, then Create.

  5. Open the API and copy the Invoke URL (https://<api-id>.execute-api.us-east-1.amazonaws.com). Open it in a browser or call it with curl. The console adds the resource policy on the function that lets API Gateway invoke it.

  6. In Monitor → Logging, select the $default stage and turn on Access logging with a CloudWatch Logs log group (create /aws/apigateway/bookshelf-api first) and the JSON format.

With the AWS CLI

The fastest path is quick create: one call creates the API, a $default route, a Lambda proxy integration and an auto-deploying $default stage.

export API_ID=$(aws apigatewayv2 create-api --name bookshelf-api --protocol-type HTTP \
  --target "$FN_ARN" --query ApiId --output text)

# Quick create does not add the invoke permission; do it with a source ARN scoped to this API
aws lambda add-permission --function-name "$FN_NAME" --statement-id apigw-invoke-"$API_ID" \
  --action lambda:InvokeFunction --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:$AWS_REGION:$ACCOUNT_ID:$API_ID/*/*/*"

export API_URL=$(aws apigatewayv2 get-api --api-id "$API_ID" --query ApiEndpoint --output text)
curl -s "$API_URL/"

Derived from aws apigatewayv2 create-api and aws lambda add-permission. The --target shortcut gives every method and path to the same function. For a real API, create the pieces explicitly so each route can have its own integration, authorizer and throttling:

export API_ID=$(aws apigatewayv2 create-api --name bookshelf-api --protocol-type HTTP \
  --query ApiId --output text)

# Lambda proxy integration, payload format 2.0
export PING_INT=$(aws apigatewayv2 create-integration --api-id "$API_ID" \
  --integration-type AWS_PROXY --integration-uri "$FN_ARN" \
  --payload-format-version 2.0 --query IntegrationId --output text)

aws apigatewayv2 create-route --api-id "$API_ID" --route-key 'GET /ping' \
  --target "integrations/$PING_INT"

# Log group, then a stage with auto-deploy and access logs
aws logs create-log-group --log-group-name /aws/apigateway/bookshelf-api
aws logs put-retention-policy --log-group-name /aws/apigateway/bookshelf-api --retention-in-days 30
aws apigatewayv2 create-stage --api-id "$API_ID" --stage-name prod --auto-deploy \
  --access-log-settings '{"DestinationArn":"arn:aws:logs:us-east-1:111122223333:log-group:/aws/apigateway/bookshelf-api","Format":"{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"routeKey\":\"$context.routeKey\",\"status\":\"$context.status\",\"integrationStatus\":\"$context.integrationStatus\",\"responseLength\":\"$context.responseLength\",\"error\":\"$context.error.message\"}"}'

aws lambda add-permission --function-name "$FN_NAME" --statement-id apigw-invoke-"$API_ID" \
  --action lambda:InvokeFunction --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:$AWS_REGION:$ACCOUNT_ID:$API_ID/*/GET/ping"

curl -s "https://$API_ID.execute-api.$AWS_REGION.amazonaws.com/prod/ping"

Derived from create-integration, create-route and create-stage. Note the stage name in the URL (/prod/ping); with $default it would be /ping. The Lambda permission is scoped by --source-arn to the single API, stage wildcard, method and path so no other API in the account can invoke the function; a quick-create wildcard like ///* is fine for a demo but too broad for production.

A Lambda handler for payload format 2.0

package com.example.bookshelf.edge;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;
import java.util.Map;

public class PingHandler implements RequestHandler<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> {
    @Override
    public APIGatewayV2HTTPResponse handleRequest(APIGatewayV2HTTPEvent event, Context context) {
        var claims = event.getRequestContext().getAuthorizer() == null
            ? Map.<String, String>of()
            : event.getRequestContext().getAuthorizer().getJwt().getClaims();
        return APIGatewayV2HTTPResponse.builder()
            .withStatusCode(200)
            .withHeaders(Map.of("content-type", "application/json"))
            .withBody("{\"pong\":true,\"sub\":\"" + claims.getOrDefault("sub", "anonymous") + "\"}")
            .build();
    }
}

The types come from com.amazonaws:aws-lambda-java-events; see Using Lambda with API Gateway. Authenticated claims arrive in requestContext.authorizer.jwt.claims; Lambda packaging, SnapStart for Java and the aws-serverless-java-container adapter for Spring Boot are on AWS Lambda.

catalog-api and orders-api stay private: the ALB alb-bookshelf is internal, with no public address. An HTTP API reaches it through a VPC link (V2), which makes API Gateway create and manage elastic network interfaces in your subnets. The link is immutable after creation (subnets and security groups cannot be changed), is reusable across APIs, becomes INACTIVE after 60 days without traffic (its interfaces are deleted and requests fail until it is reprovisioned), and requires that the link, the load balancer and the API are in the same account.

In the AWS Management Console

  1. In API Gateway choose VPC links → Create, choose the HTTP API type (V2), name it bookshelf-vpclink, select the VPC, two private subnets in different Availability Zones and a security group sg-bookshelf-vpclink, then Create. Wait until the status is Available (a few minutes).

  2. In the EC2 console, edit the ALB’s security group so it allows inbound HTTPS from sg-bookshelf-vpclink.

  3. Open bookshelf-api → Integrations → Create. Choose Private resource, ALB/NLB, select the load balancer alb-bookshelf and its HTTPS listener, the VPC link, and Advanced settings → Secure server name (catalog.internal.example.com, the name on the certificate) because private integrations default to plain HTTP.

  4. In Routes create ANY /catalog/{proxy+} and attach that integration. Under the integration’s Parameter mapping overwrite the path with /${request.path.proxy} if the application does not expect the /catalog prefix.

  5. Open the stage and check the Invoke URL; test with curl "$API_URL/catalog/books".

With the AWS CLI

export VPC_ID=vpc-0123456789abcdef0                  # placeholders; use your own
export SUBNET_A=subnet-0aaa1111bbbb22223 SUBNET_B=subnet-0ccc3333dddd44445

export VPCLINK_SG=$(aws ec2 create-security-group --group-name sg-bookshelf-vpclink \
  --description "API Gateway VPC link" --vpc-id "$VPC_ID" --query GroupId --output text)

export VPC_LINK_ID=$(aws apigatewayv2 create-vpc-link --name bookshelf-vpclink \
  --subnet-ids "$SUBNET_A" "$SUBNET_B" --security-group-ids "$VPCLINK_SG" \
  --query VpcLinkId --output text)
aws apigatewayv2 get-vpc-link --vpc-link-id "$VPC_LINK_ID" --query VpcLinkStatus   # wait for AVAILABLE

# Let the VPC link reach the ALB listener
aws ec2 authorize-security-group-ingress --group-id "$ALB_SG" --protocol tcp --port 443 \
  --source-group "$VPCLINK_SG"

export CATALOG_INT=$(aws apigatewayv2 create-integration --api-id "$API_ID" \
  --integration-type HTTP_PROXY --integration-method ANY \
  --connection-type VPC_LINK --connection-id "$VPC_LINK_ID" \
  --integration-uri "$LISTENER_ARN" --payload-format-version 1.0 \
  --tls-config ServerNameToVerify=catalog.internal.example.com \
  --request-parameters 'overwrite:path=/$request.path.proxy' \
  --query IntegrationId --output text)

aws apigatewayv2 create-route --api-id "$API_ID" --route-key 'ANY /catalog/{proxy+}' \
  --target "integrations/$CATALOG_INT"

Derived from aws apigatewayv2 create-vpc-link, create-integration and Create private integrations for HTTP APIs. The $ALB_SG variable is the ALB’s security group ID. For private integrations API Gateway includes the stage name in the request path sent to the back end (a request to /prod/catalog/books arrives as prod/catalog/books/…​), which is why the integration above overwrites the path; with the $default stage there is no prefix to remove. The same VPC link can serve a second integration for orders-api on a different ALB listener rule. Private integrations also work with Network Load Balancers and, for ECS Service Connect or service discovery, with AWS Cloud Map services; pick the combination with Load Balancers and Gateways and ECS Deployments, Scaling and Networking.

REST APIs

A REST API is more verbose than an HTTP API: resources and methods instead of route keys, a deployment you create explicitly, and a stage that points at a deployment. You get more in return.

Importing an OpenAPI definition

openapi: "3.0.1"
info:
  title: bookshelf-partner-api
  version: "1.0"
paths:
  /books:
    get:
      operationId: listBooks
      parameters:
        - name: author
          in: query
          required: false
          schema: { type: string }
      responses:
        "200": { description: OK }
      security:
        - api_key: []
      x-amazon-apigateway-request-validator: params
      x-amazon-apigateway-integration:
        type: http_proxy
        httpMethod: GET
        uri: https://catalog.internal.example.com/books
        connectionType: VPC_LINK
        connectionId: "${stageVariables.vpcLinkId}"
        passthroughBehavior: when_no_match
components:
  securitySchemes:
    api_key:
      type: apiKey
      name: x-api-key
      in: header
x-amazon-apigateway-request-validators:
  params:
    validateRequestParameters: true
    validateRequestBody: false

The x-amazon-apigateway-* extensions describe integrations, validators, authorizers and CORS. HTTP APIs import OpenAPI 3.0 with aws apigatewayv2 import-api --body file://openapi.yaml (and ignore unsupported extensions with warnings); REST APIs use aws apigateway import-rest-api. See OpenAPI extensions for API Gateway.

In the AWS Management Console

  1. Open API Gateway → REST API → Import (or Build for a blank API). Choose Import API, upload openapi.yaml, set the API endpoint type (Regional for Bookshelf) and choose Create API.

  2. Review Resources: each path and method shows its integration request, authorizer and request validator.

  3. Choose Deploy API, New stage → prod, and Deploy. The stage page shows the Invoke URL.

  4. In the stage Stage variables tab add vpcLinkId (with the ID of a REST-compatible VPC link), and in Logs and tracing turn on access logging, optional execution logging and X-Ray tracing.

  5. In Usage plans create partners with a rate, burst and monthly quota, attach the prod stage, and add an API key.

With the AWS CLI

export REST_ID=$(aws apigateway import-rest-api --body fileb://openapi.yaml \
  --parameters endpointConfigurationTypes=REGIONAL --query id --output text)

# A deployment is an immutable snapshot; a stage points at it
aws apigateway create-deployment --rest-api-id "$REST_ID" --stage-name prod \
  --variables vpcLinkId="$VPC_LINK_ID" --description "first deployment"

# Update the definition later, then redeploy
aws apigateway put-rest-api --rest-api-id "$REST_ID" --mode overwrite --body fileb://openapi.yaml
aws apigateway create-deployment --rest-api-id "$REST_ID" --stage-name prod

# Stage settings: method-level throttling and metrics
aws apigateway update-stage --rest-api-id "$REST_ID" --stage-name prod --patch-operations \
  op=replace,path=/*/*/throttling/rateLimit,value=100 \
  op=replace,path=/*/*/throttling/burstLimit,value=50 \
  op=replace,path=/*/*/metrics/enabled,value=true

Derived from aws apigateway import-rest-api, create-deployment, put-rest-api and update-stage. A frequent REST API surprise: editing a method or integration changes nothing until you create a new deployment; HTTP APIs with auto-deploy avoid that step. Stage variables (up to 100 per stage, 512 characters per value) let one definition target dev, test and prod back ends, and are referenced as ${stageVariables.name} in integration URIs and mapping templates; do not put secrets in them (see Secrets Manager, Parameter Store and KMS).

Canary releases and response streaming

A canary release deploys a new deployment to a percentage of a REST API stage’s traffic while the rest keeps hitting the previous one, with separate metrics and logs:

aws apigateway create-deployment --rest-api-id "$REST_ID" --stage-name prod \
  --canary-settings percentTraffic=10,useStageCache=false
# Looks healthy: promote it (the canary deployment becomes the stage deployment), then end the canary
aws apigateway update-stage --rest-api-id "$REST_ID" --stage-name prod \
  --patch-operations op=copy,from=/canarySettings/deploymentId,path=/deploymentId \
  op=remove,path=/canarySettings

Derived from Set up a canary release deployment. The console’s Promote canary action does the same promotion. HTTP APIs have no canary; use two stages, weighted Lambda aliases (AWS Lambda), or weighted ALB target groups.

Response streaming (REST APIs only, a recent addition) sets the integration’s responseTransferMode to STREAM for HTTP_PROXY and AWS_PROXY integrations, so API Gateway forwards bytes as the back end produces them. It lowers time to first byte for generative AI chat and server-sent events, allows responses beyond 10 MB and beyond the 29 second integration timeout, up to 15 minutes, and is billed separately. It does not support caching, API Gateway content encoding or VTL response mapping, and request streaming is not supported. Streams hit an idle timeout of 5 minutes on Regional and private endpoints (30 seconds on edge-optimized). Details: Stream the integration response; for Lambda streaming itself see AWS Lambda.

Private APIs

A REST API with the Private endpoint type is reachable only from a VPC through an interface VPC endpoint for execute-api (or through AWS PrivateLink from other networks). Its resource policy decides which VPCs, VPC endpoints, accounts or IP ranges may call it, and the API is not callable until that policy exists and the API is deployed. For Bookshelf’s internal admin API:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "execute-api:/*"
    },
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "execute-api:/*",
      "Condition": {
        "StringNotEquals": { "aws:SourceVpce": "vpce-0123456789abcdef0" }
      }
    }
  ]
}

Apply it with aws apigateway update-rest-api --rest-api-id "$REST_ID" --patch-operations op=replace,path=/policy,value='<escaped policy JSON>' and redeploy. The pattern (an explicit Allow plus a Deny unless the call arrives through your endpoint) is the documented one: Control access to a REST API with resource policies. HTTP APIs cannot be private; keep an HTTP API’s back end private with a VPC link instead, as above.

WebSocket APIs

A WebSocket API receives JSON messages and chooses a route from a route selection expression such as ${request.body.action}. Three routes are predefined: $connect (the place to attach an authorizer, because authentication happens once when the connection opens), $disconnect and $default (no matching route). Your back end pushes data to a client by calling the connection URL of the @connections API with the connection ID it stored (typically in DynamoDB, Amazon DynamoDB).

export WS_ID=$(aws apigatewayv2 create-api --name bookshelf-live --protocol-type WEBSOCKET \
  --route-selection-expression '$request.body.action' --query ApiId --output text)

export WS_INT=$(aws apigatewayv2 create-integration --api-id "$WS_ID" --integration-type AWS_PROXY \
  --integration-uri "arn:aws:apigateway:$AWS_REGION:lambda:path/2015-03-31/functions/$FN_ARN/invocations" \
  --query IntegrationId --output text)
aws apigatewayv2 create-route --api-id "$WS_ID" --route-key '$connect' --target "integrations/$WS_INT"
aws apigatewayv2 create-route --api-id "$WS_ID" --route-key '$disconnect' --target "integrations/$WS_INT"
aws apigatewayv2 create-stage --api-id "$WS_ID" --stage-name prod --auto-deploy

Derived from aws apigatewayv2 create-api and the WebSocket API overview. Limits to design around: connections end after 10 minutes of idleness and after 2 hours in total (clients must reconnect), and binary frames are not supported on WebSocket APIs. Bookshelf’s notifier could push order status over a WebSocket, but Server-Sent Events over a REST API with response streaming, or polling, are often simpler; choose WebSocket when the client also sends frequent messages.

Authorization and authentication

Mechanism How it works Choose it when

JWT authorizer (HTTP APIs)

API Gateway validates a JWT from an OpenID Connect or OAuth 2.0 provider: signature (RSA algorithms, key fetched from the issuer’s jwks_uri and cached up to two hours), iss, aud or client_id, exp, nbf, iat and, if the route lists scopes, that the token carries at least one of them.

Clients sign in with Cognito, Auth0, Keycloak, Entra ID or the Bookshelf’s own Spring Authorization Server and send a bearer token. The default for user-facing APIs.

Lambda authorizer

Your function receives the token or request attributes and returns allow/deny plus context. HTTP APIs support a simple boolean response; REST APIs expect an IAM policy document. Results can be cached by identity source.

Opaque tokens, API keys checked against a database, multi-tenant rules, or JWT validation on REST APIs.

IAM (SigV4)

The caller signs the request with AWS credentials; API Gateway checks an identity-based policy for execute-api:Invoke.

Service-to-service calls inside AWS, partner accounts that assume a role, and CLI or SDK clients.

Cognito user pool authorizer (REST APIs)

API Gateway validates the access or ID token of a Cognito user pool. On HTTP APIs use a JWT authorizer with the pool as issuer.

You use Cognito and a REST API.

Resource policy (REST APIs)

A JSON policy on the API restricts source accounts, IP ranges or VPC endpoints.

Private APIs and IP allow-lists.

Mutual TLS

Clients present a certificate that is validated against a truststore in S3 on the custom domain.

Machine-to-machine clients with certificates.

API keys are not in this table on purpose; see API keys are not authentication.

JWT authorizer with an OIDC provider

The token arrives in the Authorization header. The token types, scopes and audiences behind the checks above are explained in JWT and the JOSE Family, OpenID Connect, ID Tokens vs. Access Tokens and Scopes, Claims and Permissions; discovery and the jwks_uri are in Discovery, Metadata and Client Registration, and a client-credentials flow for service callers in Client Credentials and Client Authentication. API Gateway cannot tell an access token from an ID token, so require scopes on routes (or a dedicated audience) rather than accepting any validly signed token.

In the AWS Management Console

  1. Open bookshelf-api → Authorization → Manage authorizers → Create.

  2. Choose JWT, name it bookshelf-oidc, keep Identity source $request.header.Authorization, set Issuer URL to the provider’s issuer (for Cognito https://cognito-idp.us-east-1.amazonaws.com/<user-pool-id>;) and add the Audience (the app client ID or the API identifier).

  3. Back on Authorization → Attach authorizers to routes, select a route such as ANY /catalog/{proxy+}, pick bookshelf-oidc and choose Attach authorizer.

  4. Open the route → Authorization and add Authorization scopes such as catalog.read.

With the AWS CLI

export AUTHZ_ID=$(aws apigatewayv2 create-authorizer --api-id "$API_ID" --name bookshelf-oidc \
  --authorizer-type JWT --identity-source '$request.header.Authorization' \
  --jwt-configuration Audience=bookshelf-web,Issuer=https://auth.example.com/ \
  --query AuthorizerId --output text)

export ROUTE_ID=$(aws apigatewayv2 get-routes --api-id "$API_ID" \
  --query "Items[?RouteKey=='ANY /catalog/{proxy+}'].RouteId | [0]" --output text)

aws apigatewayv2 update-route --api-id "$API_ID" --route-id "$ROUTE_ID" \
  --authorization-type JWT --authorizer-id "$AUTHZ_ID" --authorization-scopes catalog.read

Derived from aws apigatewayv2 create-authorizer and Control access to HTTP APIs with JWT authorizers. Use the issuer exactly as in the token’s iss claim, including any trailing slash. Quotas to know: 10 authorizers per API, 50 audiences per authorizer, 10 scopes per route, and a 1.5 second timeout when API Gateway fetches the discovery document or key set. After key rotation allow a grace period during which both keys are valid, because API Gateway may use a cached key for up to two hours.

The back end should still validate the token (defense in depth) and enforce fine-grained authorization; the gateway check only stops anonymous traffic early. With Spring Boot as a resource server that is a single issuer property, see Spring Security and Authorization Server & Social Login:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/

Lambda authorizer

For tokens that are not JWTs, or for rules API Gateway cannot express, attach a REQUEST authorizer. With payload format 2.0 and simple responses the function returns a boolean and optional context passed to the integration:

aws apigatewayv2 create-authorizer --api-id "$API_ID" --name bookshelf-lambda-authz \
  --authorizer-type REQUEST --identity-source '$request.header.x-bookshelf-token' \
  --authorizer-uri "arn:aws:apigateway:$AWS_REGION:lambda:path/2015-03-31/functions/arn:aws:lambda:$AWS_REGION:$ACCOUNT_ID:function:bookshelf-authz/invocations" \
  --authorizer-payload-format-version 2.0 --enable-simple-responses \
  --authorizer-result-ttl-in-seconds 300

aws lambda add-permission --function-name bookshelf-authz --statement-id apigw-authz \
  --action lambda:InvokeFunction --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:$AWS_REGION:$ACCOUNT_ID:$API_ID/authorizers/*"

Derived from the HTTP API Lambda authorizer guide. The function must answer within 10 seconds and returns { "isAuthorized": true, "context": { "tenant": "acme" } }; the cache key is built from the identity sources, so keep them to the values that actually decide the answer. REST APIs use TOKEN or REQUEST authorizers that must return an IAM policy document instead.

IAM (SigV4) authorization

For an internal caller (for example orders-api calling the admin API), set the route’s authorization type to AWS_IAM and give the caller’s role permission to invoke it:

aws apigatewayv2 update-route --api-id "$API_ID" --route-id "$ROUTE_ID" --authorization-type AWS_IAM
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:us-east-1:111122223333:abc123defg/prod/GET/admin/*"
    }
  ]
}

Attach the policy to the task role or Lambda execution role of the caller (Identity and Access); the resource format is arn:aws:execute-api:<region>:<account>:<api-id>/<stage>/<METHOD>/<path>, with abc123defg a placeholder API ID. The caller must sign requests with SigV4 for the service name execute-api; the AWS SDK for Java 2.x does this with its AwsV4HttpSigner, and curl with --aws-sigv4 "aws:amz:us-east-1:execute-api" plus temporary credentials. Reference: Control access to HTTP APIs with IAM authorization.

Cognito

Amazon Cognito user pools are managed OIDC providers (sign-up, sign-in, MFA, social federation) and identity pools exchange tokens for AWS credentials. For a Bookshelf API, a user pool is a reasonable issuer: point a JWT authorizer at https://cognito-idp.us-east-1.amazonaws.com/<user-pool-id>; with the app client ID as audience. Cognito access tokens carry client_id (not aud) and scope, which the JWT authorizer handles. The protocol side (authorization code with PKCE for the SPA) is in Authorization Code and PKCE and Browser-Based Apps (SPAs); the IAM-side summary is in Identity and Access.

Traffic control

Throttling

API Gateway uses a token bucket per Region per account: 10,000 requests per second steady state with a bucket of 5,000 across all APIs in the account in most Regions, the same pool for every API type, and Regions launched later start lower (2,500 and 1,250). The rate can be raised by request; the burst cannot be changed independently. You can set lower targets per stage and per route so one noisy API does not use the account’s whole allowance:

# HTTP API: stage default and one route
aws apigatewayv2 update-stage --api-id "$API_ID" --stage-name prod \
  --default-route-settings ThrottlingBurstLimit=200,ThrottlingRateLimit=100
aws apigatewayv2 update-stage --api-id "$API_ID" --stage-name prod \
  --route-settings '{"POST /orders":{"ThrottlingBurstLimit":50,"ThrottlingRateLimit":20}}'

Derived from Throttle requests to your HTTP APIs. Throttles are best-effort targets, not guarantees; clients receive 429 Too Many Requests and should retry with exponential backoff and jitter. A route-level limit cannot exceed the account limit. REST APIs offer the same levels (account, stage, method) plus per-client limits through usage plans.

Usage plans and API keys

A REST API usage plan defines a throttle (rate and burst) and an optional quota (requests per day, week or month) and applies it to the API keys attached to the plan, for example a partners plan for third-party integrators:

export PLAN_ID=$(aws apigateway create-usage-plan --name partners \
  --throttle burstLimit=20,rateLimit=10 --quota limit=100000,period=MONTH \
  --api-stages apiId="$REST_ID",stage=prod --query id --output text)
export KEY_ID=$(aws apigateway create-api-key --name partner-acme --enabled --query id --output text)
aws apigateway create-usage-plan-key --usage-plan-id "$PLAN_ID" --key-id "$KEY_ID" --key-type API_KEY
aws apigateway get-api-key --api-key "$KEY_ID" --include-value --query value --output text

Derived from aws apigateway create-usage-plan. Methods must have API key required set (apiKeyRequired: true, or the security: api_key entry in the OpenAPI above), and the key is sent in the x-api-key header. API keys and usage plans exist only for REST APIs.

API keys are not authentication

An API key identifies a client for metering and throttling; it is a shared, long-lived string that ends up in configuration files, logs and mobile apps. It does not identify a user, is not scoped, and offers no signature or expiry. Use it to meter and throttle, always together with a real authorizer (JWT, IAM or Lambda). The official documentation says the same.

AWS WAF

REST APIs support attaching an AWS WAF web ACL to a stage (managed rule groups, rate-based rules, IP sets). HTTP APIs do not; to protect an HTTP API with WAF, put CloudFront in front of it with a WAF web ACL and have the API accept only traffic carrying a secret header or coming through CloudFront (see CloudFront and CDN Delivery), and disable the default execute-api endpoint (below) so the API cannot be called around the CDN.

Request handling

CORS

Browsers call bookshelf-api from the CloudFront site, so cross-origin requests need the right headers. With an HTTP API, configure CORS on the API and API Gateway answers preflight OPTIONS requests itself (an OPTIONS route is not needed) and adds the headers to responses:

aws apigatewayv2 update-api --api-id "$API_ID" --cors-configuration \
  AllowOrigins=https://www.example.com,AllowMethods=GET,POST,AllowHeaders=authorization,content-type,MaxAge=3600

Derived from Configure CORS for an HTTP API. When CORS is configured on the API, API Gateway ignores CORS headers set by the integration; do not set them in both places. Use specific origins; * cannot be combined with credentials. On REST APIs you add an OPTIONS mock method and return the CORS headers from the integration or from gateway responses (including error responses, or a 401 will look like a CORS failure in the browser). For Spring Boot behind the gateway see the Spring Security page above.

Validation, mapping and transformation

  • HTTP APIs have parameter mapping only: modify (overwrite, append, remove) request headers, query strings and the path using context and stage variables, and map response headers and status codes. Everything else (body validation) belongs in the application.

  • REST APIs add request validators (check required parameters and a JSON Schema model before the integration is invoked and return 400 cheaply), models, and VTL mapping templates to reshape bodies. Templates are powerful and hard to test; for a Lambda or HTTP proxy integration, prefer to pass the request through unchanged and do the work in code. Use mapping templates for direct AWS service integrations (for example writing to SQS or DynamoDB without a Lambda) where there is no code to put the logic in.

Stages, custom domains and routing

Custom domain

The default https://<api-id>.execute-api.<region>.amazonaws.com URL is fine for tests; production uses a name you own, with an ACM certificate (certificate management and DNS are on Route 53, DNS and ACM Certificates). For a Regional custom domain the certificate must be in the same Region as the API (for edge-optimized REST domains it must be in us-east-1).

In the AWS Management Console

  1. In ACM in us-east-1 request or import a certificate for api.example.com and validate it with DNS.

  2. In API Gateway → Custom domain names → Create, enter api.example.com, choose the TLS 1.2 security policy, the Regional endpoint type and the certificate, then Create.

  3. In API mappings → Configure API mappings map bookshelf-api and stage prod to the empty path (or v1).

  4. Copy the API Gateway domain name and its hosted zone ID. In Route 53 create an A (alias) record api.example.com that points to Alias to API Gateway API in the Region.

  5. Open the API’s Stages and, to force traffic through the custom domain, disable the default endpoint (API settings → Disable default endpoint).

With the AWS CLI

export CERT_ARN=arn:aws:acm:us-east-1:111122223333:certificate/00000000-0000-0000-0000-000000000000

aws apigatewayv2 create-domain-name --domain-name api.example.com \
  --domain-name-configurations CertificateArn="$CERT_ARN",EndpointType=REGIONAL,SecurityPolicy=TLS_1_2

aws apigatewayv2 create-api-mapping --domain-name api.example.com --api-id "$API_ID" --stage prod \
  --api-mapping-key v1

aws apigatewayv2 get-domain-name --domain-name api.example.com \
  --query 'DomainNameConfigurations[0].[ApiGatewayDomainName,HostedZoneId]' --output text

# Optional: only the custom domain is callable
aws apigatewayv2 update-api --api-id "$API_ID" --disable-execute-api-endpoint

Derived from aws apigatewayv2 create-domain-name, create-api-mapping and Custom domain names for HTTP APIs. The two values printed by get-domain-name feed an alias record in Route 53 (aws route53 change-resource-record-sets with AliasTarget). An API mapping attaches an API and stage to a path under the domain, so https://api.example.com/v1/catalog/books reaches stage prod of bookshelf-api. One domain can serve several APIs by using different keys. Disabling the default endpoint is important when WAF, throttling or mutual TLS are applied on the custom domain path only. Mutual TLS is enabled on the domain with --mutual-tls-authentication TruststoreUri=s3://…​.

Routing rules

API mappings choose the target by path only. Routing rules (introduced in 2025) let a custom domain choose a REST API stage by request headers and base path, with a priority, so you can send x-version: beta to a staging stage or split a legacy and a new API behind one domain without redeploying clients:

aws apigatewayv2 update-domain-name --domain-name api.example.com \
  --routing-mode ROUTING_RULE_THEN_API_MAPPING

aws apigatewayv2 create-routing-rule --domain-name api.example.com --priority 100 \
  --conditions '[{"MatchHeaders":{"AnyOf":[{"Header":"x-version","ValueGlob":"beta"}]}}]' \
  --actions '[{"InvokeApi":{"ApiId":"abc123defg","Stage":"beta"}}]'

Derived from Routing rules to connect API stages to a custom domain name and its usage page. Rules are evaluated from the lowest priority value to the highest (leave gaps between priorities), each rule allows up to two header conditions and one base-path condition and one action, and the target must be in the same account. The routing mode can be API_MAPPING_ONLY, ROUTING_RULE_THEN_API_MAPPING or ROUTING_RULE_ONLY. Public domains route only to public APIs and private domains only to private ones; a domain that has API mappings to both REST and HTTP APIs does not support routing rules. Check the current page for the supported API types before relying on this for HTTP APIs.

Networking and layered topologies

  • Endpoint types. HTTP APIs are Regional. REST APIs are Regional (best when your clients or your own CloudFront distribution are in one Region), edge-optimized (API Gateway puts a managed CloudFront distribution in front; the certificate for a custom domain must be in us-east-1) or private.

  • Back-end reachability. Lambda integrations need no networking. Private HTTP back ends use a VPC link (above); the ALB should be internal and its security group should allow only the VPC link’s security group. A public back end works with plain HTTP_PROXY URLs, but then anyone can call it directly; protect it with a header secret or mutual TLS, or make it private.

  • Layers. A common production shape is CloudFront → API Gateway → Lambda or VPC link → ALB → ECS: CloudFront gives caching, global entry and WAF for HTTP APIs, API Gateway gives authorization and per-route limits, and the ALB gives health checks and connection to containers. Do not stack gateways for no reason, each hop adds latency and cost; when every request goes to containers on ECS and you need no per-client management, an ALB with authentication rules (see Load Balancers and Gateways) can be enough.

  • VPC egress and DNS. API Gateway runs outside your VPC and reaches private resources only through VPC links; it has no static outbound IP address. Back ends that must allow-list the gateway use a VPC link and security groups. The VPC pieces (subnets, security groups, endpoints) are on VPC Networking.

Operations

  • Access logs. Turn them on for every stage (the guide recommends it and Security Hub flags stages without logging). Use JSON format with $context.requestId, $context.routeKey, $context.status, $context.integrationStatus, $context.integrationErrorMessage, $context.error.message and $context.authorizer.error, and set a retention period. Log the same request ID in the application to correlate gateway and back-end records.

  • REST API execution logs (ERROR or INFO) additionally record the integration request and response; they need an account-level IAM role for CloudWatch (aws apigateway update-account --patch-operations op=replace,path=/cloudwatchRoleArn,value=<role-arn>) and can contain sensitive data, so keep them at ERROR in production.

  • Metrics. CloudWatch namespace AWS/ApiGateway reports Count, 4xx/4XXError, 5xx/5XXError, Latency and IntegrationLatency per API and stage (and per route when detailed metrics are on). Alarm on 5xx rate and on the gap between Latency and IntegrationLatency, which is time spent in API Gateway itself (authorizers, mapping).

  • Tracing. REST APIs support AWS X-Ray tracing per stage; HTTP APIs do not. For HTTP APIs, start the trace in the back end (the Lambda active tracing, or an OpenTelemetry SDK or the AWS Distro for OpenTelemetry in the container), propagate the incoming x-amzn-trace-id or W3C traceparent headers, and join traces by the request ID. See Monitoring and Observability and, for the application side, Metrics and Observability: Micrometer, Actuator, and Distributed Tracing.

  • Deployments. Keep the definition in code: SAM, CloudFormation or CDK (Infrastructure as Code). An HTTP API with a JWT authorizer, a Lambda route and a stage, as SAM:

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
  BookshelfApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: prod
      Auth:
        DefaultAuthorizer: OidcAuthorizer
        Authorizers:
          OidcAuthorizer:
            IdentitySource: $request.header.Authorization
            JwtConfiguration:
              issuer: https://auth.example.com/
              audience: [bookshelf-web]
      CorsConfiguration:
        AllowOrigins: ["https://www.example.com"]
        AllowMethods: [GET, POST]
        AllowHeaders: [authorization, content-type]
  PingFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: java25
      Handler: com.example.bookshelf.edge.PingHandler::handleRequest
      CodeUri: target/ping.jar
      Events:
        Ping:
          Type: HttpApi
          Properties:
            ApiId: !Ref BookshelfApi
            Path: /ping
            Method: GET

Derived from the SAM AWS::Serverless::HttpApi reference; sam deploy and sam delete are covered on AWS Lambda. SAM also creates the Lambda invoke permission for you.

Limits and quotas

Quotas are Region-scoped and change; read the current values with aws service-quotas list-service-quotas --service-code apigateway and the pages in the references. The numbers below were checked on 2026-10-01.

Limit HTTP API REST API WebSocket API

Account throttle (all types, per Region)

10,000 requests per second, bucket of 5,000 (lower in some newer Regions); rate adjustable on request

Same pool

Maximum integration timeout

30 seconds (fixed)

29 seconds default; for Regional and private APIs it can be raised at the cost of account throttle quota

29 seconds per integration call; connection idle timeout 10 minutes; maximum connection duration 2 hours

Payload size

10 MB

10 MB (more with response streaming)

Messages up to 128 KB (frames of 32 KB)

Routes or resources per API

300 routes (adjustable)

300 resources (adjustable)

Per-API route quota

Stages per API

10 (adjustable)

10 (adjustable)

10 (adjustable)

Authorizers per API

10

10 (Lambda and Cognito)

10

VPC links per account per Region

10 (adjustable)

20 (adjustable)

Not applicable

Custom domain names per account per Region

120 public (adjustable); 50 private for REST APIs

Same pool

Rows for WebSocket values come from the WebSocket pages of the developer guide rather than the HTTP/REST quota pages; confirm them against the current quota page before sizing a design. Other limits that bite in practice: caching is REST only with a 300 second default TTL (0 to 3600) and a 1 MB cached response; the REST API idle connection timeout is 310 seconds; API Gateway control-plane calls (creating deployments, domains, VPC links) have their own, small rate limits that matter for CI pipelines that create and destroy many APIs; and the combined request-line-and-header size is 10,240 bytes on HTTP APIs and 20,480 on REST APIs.

The 29 and 30 second limits decide architecture: a synchronous request that may run longer (report generation, large exports) should return 202 Accepted and a status URL, push work to SQS or Step Functions (Lambda: Event-Driven and Serverless Apps), or use REST response streaming.

Pricing model

No prices are stated here; use the API Gateway pricing page and the AWS Pricing Calculator for current figures (checked 2026-10-01).

  • HTTP APIs are billed per million requests received, metered in 512 KB increments of request payload, and cost noticeably less per request than REST APIs. Tiered per-million prices fall as monthly volume grows.

  • REST APIs are billed per million requests received, with the cost of optional features on top: an hourly charge for the cache sized by instance type, data transfer out, and charges for response streaming.

  • WebSocket APIs are billed per million messages (metered in 32 KB increments, up to 128 KB) plus connection minutes.

  • Free tier. New accounts have had an allowance of millions of calls for the first twelve months; AWS has changed how free-tier allowances are granted for new accounts, so read the current terms on the pricing page.

  • Not on the API Gateway line item but part of the bill: data transfer out (EC2 data-transfer rates), the Lambda invocations that the API causes, CloudWatch Logs ingestion for access and execution logs, Lambda authorizer invocations (cache their results), the VPC link’s network interfaces and cross-AZ traffic, AWS PrivateLink and interface endpoint hours for private APIs, WAF web ACL and request charges, ACM public certificates (free) and the Route 53 hosted zone.

  • Control levers. Use an HTTP API unless you need a REST-only feature; cache and CloudFront-cache GET responses; set log retention; cache authorizer results; avoid chatty clients with throttling; delete unused stages, APIs and VPC links (an unused VPC link still holds network interfaces and an unused regional custom domain keeps its certificate tied up).

See Cost Management and Support for budgets and anomaly detection.

Troubleshooting

Start with the access log and the response headers; x-amzn-requestid and x-amz-apigw-id in the response identify the request for AWS Support. Test the integration directly (call the ALB or invoke the Lambda) to see whether the fault is in API Gateway or behind it.

Symptom Likely causes and fixes

403 Forbidden, body {"message":"Forbidden"}

A route or resource that does not exist on a REST API (it answers 403 rather than 404 for missing paths), a missing API key, the wrong custom-domain mapping, WAF blocking, a resource policy deny, or a private API called from outside its VPC endpoint. Check $context.error.message in the access log.

401 Unauthorized with a JWT authorizer

No Authorization header, an expired token, the wrong iss (compare byte for byte, including trailing slash), aud or client_id not in the configured audience, a missing required scope, or an ID token sent in place of an access token. Decode the token (without trusting it) and compare claims; see Testing and Debugging OAuth.

403 with IAM authorization

Unsigned request, signature for the wrong service or Region, expired temporary credentials, or the caller’s policy missing execute-api:Invoke on that exact stage, method and path ARN.

404 Not Found on HTTP APIs

No route matches the method and path; check the stage prefix in the URL and whether the stage name or API mapping key is part of the path.

429 Too Many Requests

Stage, route, usage plan or account throttle reached. Look at the Count metric, raise the limit or smooth the client with retries and jitter.

500 Internal Server Error

Misconfigured integration, invalid response from a Lambda function (for REST proxy integrations the response must contain statusCode, headers and a string body), or an authorizer error. Check execution logs and $context.integrationErrorMessage.

502 Bad Gateway

The back end closed the connection, returned a malformed response, or the VPC link cannot reach it (security group, target group health, wrong listener ARN, TLS server name mismatch). Test from an instance in the same subnets.

503 Service Unavailable

Back end unavailable or a VPC link that is INACTIVE or still PENDING; check get-vpc-link and re-send traffic.

504 Gateway Timeout

The integration did not answer within the limit (29 or 30 seconds). Make the work asynchronous or faster; a Lambda timeout longer than the API limit does not help the caller.

Browser shows a CORS error but the request reached the API

The error response (401, 403, 429, 5xx) has no CORS headers. Configure CORS on the HTTP API itself, or add the headers to REST gateway responses.

Back end sees the wrong path

Private integrations include the stage name in the forwarded path; API mappings strip the mapping key. Use parameter mapping with overwrite:path to normalize.

Lambda is not invoked, 500 with permission message

The function’s resource policy is missing or its --source-arn does not match the API, stage, method and path.

Clean up

An API, its stages and its routes cost nothing while idle, but a custom domain, a VPC link, a cache, a WAF association and the log groups can keep costing. Delete in dependency order:

# Custom domain: mappings first, then the domain, then the Route 53 record
aws apigatewayv2 list-api-mappings --domain-name api.example.com --query 'Items[].ApiMappingId' --output text
aws apigatewayv2 delete-api-mapping --domain-name api.example.com --api-mapping-id "$MAPPING_ID"
aws apigatewayv2 delete-domain-name --domain-name api.example.com

# HTTP and WebSocket APIs delete their routes, integrations, authorizers and stages with them
aws apigatewayv2 delete-api --api-id "$API_ID"
aws apigatewayv2 delete-api --api-id "$WS_ID"

# REST API and usage plan (remove the stage from the plan first)
aws apigateway delete-usage-plan-key --usage-plan-id "$PLAN_ID" --key-id "$KEY_ID"
aws apigateway delete-usage-plan --usage-plan-id "$PLAN_ID"
aws apigateway delete-api-key --api-key "$KEY_ID"
aws apigateway delete-rest-api --rest-api-id "$REST_ID"

# VPC link, then its security group and the ALB ingress rule
aws apigatewayv2 delete-vpc-link --vpc-link-id "$VPC_LINK_ID"
aws ec2 revoke-security-group-ingress --group-id "$ALB_SG" --protocol tcp --port 443 --source-group "$VPCLINK_SG"
aws ec2 delete-security-group --group-id "$VPCLINK_SG"

# Function permissions, logs, and the throwaway Lambda
aws lambda remove-permission --function-name "$FN_NAME" --statement-id apigw-invoke-"$API_ID"
aws logs delete-log-group --log-group-name /aws/apigateway/bookshelf-api
aws lambda delete-function --function-name "$FN_NAME"

Derived from aws apigatewayv2 delete-api, delete-domain-name, aws apigateway delete-rest-api and delete-vpc-link. In the console: API Gateway → select the API → Actions or Delete; Custom domain names → delete mappings, then the domain; VPC links → delete; then remove the Route 53 alias record, the ACM certificate if unused, the log group and, in Lambda, the function. Deleting a custom domain does not delete the DNS record, which would then dangle; and a VPC link cannot be deleted while integrations still use it. If you used a CloudFormation or SAM stack, run aws cloudformation delete-stack or sam delete instead.