Guide · AWS API Gateway

MCP Server API Gateway HTTP API — JWT authorizer, payload format v2, throttling, and CORS

API Gateway HTTP API (the "v2" API type) is the right choice for most MCP server endpoints: it is 70% cheaper than REST API ($1.00 per million vs $3.50), has ~10 ms less added latency, and includes a native JWT authorizer that validates tokens without any Lambda code. The trade-off is that HTTP API lacks some REST API features: no usage plans, no API key management, no per-method X-Ray tracing, no request validation, and no resource-based policies. For MCP tool endpoints where clients authenticate with a JWT (Cognito, Auth0, or any OIDC-compatible issuer), the native JWT authorizer is the fastest path — no Lambda invocation, no cold start, and no IAM policy document to maintain. The main operational pitfall is payload format: HTTP API defaults to format version 2.0, whose event shape is different from the 1.0 shape used by all REST API Lambdas — confusing any Lambda that was written for REST API and reused in an HTTP API without updating field access.

TL;DR

Use payloadFormatVersion: "2.0" (the HTTP API default) and access event.requestContext.http.method, event.rawPath, and event.rawQueryString — not event.httpMethod, event.path, event.queryStringParameters (still present but different shape). The native JWT authorizer needs only the OIDC issuer URL and audience list — no Lambda. For Lambda authorizers on HTTP API, use the simple response format ({ "isAuthorized": true }) unless you need per-route IAM policies. Throttling on HTTP API is set per-route via routeSettings, not per-method-stage as in REST API. CORS is configured at the API level and HTTP API handles OPTIONS preflight automatically — no Lambda for OPTIONS needed.

Payload format v2.0: field mapping for Lambda handlers

The most common migration mistake when moving from REST API to HTTP API is using REST API field names on the v2.0 event. The shapes are different at the top level. Lambda handlers must be updated to use the new field paths — or explicitly set payloadFormatVersion: "1.0" on the integration to keep the old shape (with a small latency overhead).

// Payload format v2.0 (HTTP API default) — event shape
const v2Event = {
  version: "2.0",
  routeKey: "POST /tools/invoke",
  rawPath: "/tools/invoke",
  rawQueryString: "sessionId=abc&debug=true",
  headers: {
    "content-type": "application/json",
    "authorization": "Bearer eyJ..."
  },
  queryStringParameters: {  // always present (empty object if none)
    sessionId: "abc",
    debug: "true"
  },
  pathParameters: { toolName: "search" },  // null if no path params
  body: '{"input":"query"}',  // always a string (not parsed)
  isBase64Encoded: false,
  requestContext: {
    accountId: "123456789",
    apiId: "abc123",
    domainName: "abc123.execute-api.us-east-1.amazonaws.com",
    http: {
      method: "POST",       // v2: here, not event.httpMethod
      path: "/tools/invoke", // v2: here, not event.path
      protocol: "HTTP/1.1",
      sourceIp: "1.2.3.4",
      userAgent: "..."
    },
    requestId: "...",
    routeKey: "POST /tools/invoke",
    stage: "prod",
    time: "18/Sep/2026:10:00:00 +0000",
    timeEpoch: 1726653600000
  },
  // JWT claims available here if a JWT authorizer is attached
  requestContext: {
    authorizer: {
      jwt: {
        claims: { sub: "user123", email: "user@example.com" },
        scopes: ["openid", "tools:invoke"]
      }
    }
  }
};

// v2.0-compatible Lambda handler
export const handler = async (event) => {
  // v2: use event.requestContext.http.method (NOT event.httpMethod)
  const method = event.requestContext.http.method;
  const path = event.rawPath;
  const body = event.body ? JSON.parse(event.body) : {};

  // JWT claims from native authorizer (no Lambda code to verify token)
  const userId = event.requestContext.authorizer?.jwt?.claims?.sub;

  return {
    statusCode: 200,
    // v2 response: headers is an object (same as v1); body must be a string
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ status: 'ok', userId })
  };
};

Native JWT authorizer: OIDC configuration

The JWT authorizer validates the token's signature, expiry, issuer, and audience without a Lambda. API Gateway fetches the OIDC discovery document from {issuer}/.well-known/openid-configuration and caches the JWKS for 2 hours. The authorizer checks: exp not in the past, iss matches the configured issuer, and aud (or client_id for some providers) contains the configured audience. Token claims are forwarded to the Lambda in event.requestContext.authorizer.jwt.claims.

// AWS CDK — HTTP API with JWT authorizer (Cognito example)
import { HttpApi, HttpMethod, HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';

const userPool = new cognito.UserPool(this, 'Users');
const userPoolClient = userPool.addClient('ApiClient');

const jwtAuthorizer = new HttpJwtAuthorizer('JwtAuth', userPool.userPoolProviderUrl, {
  jwtAudience: [userPoolClient.userPoolClientId]
});

const httpApi = new HttpApi(this, 'ToolsApi', {
  corsPreflight: {
    allowHeaders: ['Authorization', 'Content-Type'],
    allowMethods: [CorsHttpMethod.POST, CorsHttpMethod.GET, CorsHttpMethod.OPTIONS],
    allowOrigins: ['https://app.example.com'],
    maxAge: Duration.hours(1)
  }
});

httpApi.addRoutes({
  path: '/tools/invoke',
  methods: [HttpMethod.POST],
  integration: new HttpLambdaIntegration('InvokeTool', invokeToolFn),
  authorizer: jwtAuthorizer
});

// For non-CDK: Terraform / CloudFormation JwtConfiguration on AWS::ApiGatewayV2::Authorizer
// Properties:
//   AuthorizerType: JWT
//   IdentitySource: $request.header.Authorization
//   JwtConfiguration:
//     Audience: [ !Ref UserPoolClientId ]
//     Issuer: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}

// For Auth0 or other OIDC providers:
//   Issuer: https://your-domain.auth0.com/
//   Audience: ["https://api.example.com"]   # your API identifier in Auth0

HTTP API JWT authorizers do not support aud arrays by itself for providers that put audience in client_id (like some OAuth 2.0 providers). In those cases, use a Lambda authorizer with the simple response format to validate the token and return { isAuthorized: true }. The Lambda authorizer on HTTP API differs from REST API: there is no TOKEN type — only REQUEST type — and the default cache key is the full identitySource expression (typically $request.header.Authorization).

Per-route throttling and default stage settings

HTTP API throttling is configured in two layers: default stage-level throttle (applies to all routes) and per-route overrides via routeSettings. Both specify throttlingBurstLimit (maximum concurrent requests in a burst) and throttlingRateLimit (sustained requests per second). When throttled, API Gateway returns 429 Too Many Requests with a Retry-After header.

// AWS CDK — per-route throttle settings on HTTP API
const httpApi = new HttpApi(this, 'ToolsApi', {
  defaultAuthorizationScopes: ['openid'],
  // Default throttle for all routes
  throttle: {
    burstLimit: 500,
    rateLimit: 100   // sustained req/sec across all routes
  }
});

// Per-route override: stricter limit on expensive tool invocations
const stage = httpApi.defaultStage?.node.defaultChild as CfnStage;
stage.addPropertyOverride('RouteSettings', {
  'POST /tools/invoke': {
    ThrottlingBurstLimit: 50,
    ThrottlingRateLimit: 10   // max 10 req/sec for tool invocations
  },
  'GET /tools/status': {
    ThrottlingBurstLimit: 1000,
    ThrottlingRateLimit: 500  // status checks are cheap — allow more
  }
});

// Response when throttled — Lambda does NOT receive the request:
// HTTP 429 Too Many Requests
// { "message": "Too Many Requests" }
// Retry-After: 1
//
// Clients should implement exponential backoff with jitter starting at 1s.
// MCP client library should surface 429s as a retryable ToolError.

HTTP API vs REST API decision matrix

FeatureHTTP API (v2)REST API (v1)
Price per million requests$1.00$3.50
Added latency~10 ms~30 ms
Native JWT authorizerYes (no Lambda)No (Lambda required)
Lambda authorizer responseSimple (isAuthorized) or IAM policyIAM policy only
CORS supportFirst-class (no OPTIONS Lambda)Manual OPTIONS routes or mock integration
Usage plans + API keysNoYes
Request validationNoYes (JSON Schema)
Per-method X-Ray tracingNo (Lambda-side only)Yes
VPC Link targetALB or NLBNLB only
WebSocket supportNo (separate API type)No (separate API type)
Maximum integration timeout30 s (not configurable)29 s (configurable)
Payload format defaultv2.0v1.0

Failure modes reference

FailureSymptomFix
Lambda reads event.httpMethod on v2.0 payloadevent.httpMethod is undefined; route logic falls through to default/error caseUse event.requestContext.http.method for HTTP API v2.0; or set payloadFormatVersion: "1.0" on the Lambda integration to keep the old shape
JWT authorizer issuer URL with trailing slash mismatch401 Unauthorized — iss claim doesn't match configured issuerThe iss claim in the JWT must exactly match the Issuer URL in the authorizer config — trailing slash matters; check the token's iss claim with jwt.io
CORS preflight fails after adding Authorization headerBrowser preflight OPTIONS returns 403; real request never sentAdd Authorization to corsPreflight.allowHeaders in the HTTP API CORS config; HTTP API CORS does not automatically include Authorization
429 not retried by MCP clientTool call fails permanently on spike; user sees error instead of delayed resultImplement exponential backoff with jitter for 429 responses in the MCP client layer; use Retry-After header value as the initial backoff duration
Lambda authorizer returns IAM policy on HTTP APIPolicy is ignored; all requests pass through regardless of policy effectHTTP API Lambda authorizer with simple response format: return { isAuthorized: true/false }; IAM policy format is only used when authorizerPayloadFormatVersion is "1.0" and type is REQUEST
VPC Link to ALB fails health check502 Bad Gateway on all routes through VPC LinkHTTP API VPC Link requires the ALB listener and target group to be healthy; check ALB target health; ensure security group allows traffic from the VPC Link's internal CIDR