Guide · AWS Lambda
MCP Server Lambda Function URLs — no API Gateway, no 29s timeout, response streaming
Lambda Function URLs give your MCP server a stable HTTPS endpoint without the 29-second API Gateway integration timeout. Three issues trip up most Lambda-based MCP deployments: the API Gateway 29-second timeout (long-running tool calls — code execution, multi-step database queries, LLM sub-calls — are hard-killed mid-stream; Function URLs use Lambda's own 15-minute timeout limit instead), SSE buffering (API Gateway buffers the entire response body before forwarding to the client; Function URL streaming mode via InvokeWithResponseStream pushes bytes to the client as they are produced, enabling true SSE transport), and concurrency = max simultaneous connections (unlike ECS where a single task handles hundreds of concurrent SSE streams, each Lambda invocation handles exactly one request — concurrent MCP sessions consume concurrent Lambda invocations, which counts against your account's reserved-concurrency limit).
TL;DR
Create a Lambda Function URL with authType: NONE (or IAM for private deployments) and invokeMode: RESPONSE_STREAM for SSE transport. Set the Lambda timeout to the maximum your tool calls need (up to 15 minutes). Configure CORS headers on the Function URL rather than inside the Lambda handler. Monitor concurrency with ConcurrentExecutions CloudWatch metric — spike to reserved limit causes throttling (HTTP 429), not queuing.
Why Function URLs instead of API Gateway
API Gateway has a hard 29-second integration timeout that cannot be extended. Lambda Function URLs do not have a separate gateway timeout — the only limit is the Lambda function timeout (up to 900 seconds / 15 minutes). This makes Function URLs the correct choice for any MCP server where tool calls may take longer than 29 seconds.
| Dimension | API Gateway HTTP API | Lambda Function URL |
|---|---|---|
| Max timeout | 29 seconds (hard limit) | Lambda timeout — up to 900s |
| Streaming response | No (buffers entire body) | Yes (RESPONSE_STREAM invoke mode) |
| Custom domain | Yes, native | Via CloudFront + origin access control |
| Auth | JWT authorizer, IAM, Lambda authorizer | IAM (SigV4) or NONE |
| Request routing | Path and method routing | Single function per URL |
| Cost | $1.00/million requests + data transfer | Function URL requests are free; pay only for Lambda invocation |
| Throttling on burst | Per-stage rate and burst limits | Account-level reserved concurrency |
When API Gateway is still the right choice: multi-path routing (one Lambda per route), JWT authorizer against Cognito, per-route throttling, or WAF integration at the gateway layer. For a single-path MCP endpoint that needs long timeouts or SSE, Function URLs win.
Creating a Function URL with CDK
The CDK addFunctionUrl method attaches a Function URL to any Lambda function. Use invokeMode: RESPONSE_STREAM for SSE transport and authType: NONE for public MCP endpoints (add your own token-based auth inside the handler instead of relying on IAM SigV4 signing, which is impractical for third-party MCP clients).
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as lambdaNodeJs from "aws-cdk-lib/aws-lambda-nodejs";
import { Duration } from "aws-cdk-lib";
const mcpFn = new lambdaNodeJs.NodejsFunction(this, "McpFunction", {
entry: "src/lambda-mcp-handler.ts",
handler: "handler",
runtime: lambda.Runtime.NODEJS_22_X,
timeout: Duration.minutes(5), // up to 15 minutes — no gateway cap
memorySize: 512,
environment: {
NODE_ENV: "production",
MCP_TRANSPORT: "http",
},
});
const fnUrl = mcpFn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE, // public endpoint
invokeMode: lambda.InvokeMode.RESPONSE_STREAM, // required for SSE
cors: {
allowedOrigins: ["*"],
allowedHeaders: ["content-type", "authorization"],
allowedMethods: [lambda.HttpMethod.POST, lambda.HttpMethod.GET],
},
});
// Output the URL for use in CloudFront origin
new cdk.CfnOutput(this, "McpFunctionUrl", { value: fnUrl.url });
The generated URL has the form https://<id>.lambda-url.<region>.on.aws/. It is globally routable but not a custom domain — put CloudFront in front to serve it from your domain. See MCP Server CloudFront for the CloudFront configuration pattern; use CachingDisabled and compress: false on the MCP path.
Response streaming for SSE transport
Lambda's standard invocation model buffers the entire response in memory before returning it to the caller. RESPONSE_STREAM invoke mode changes this: the handler receives a ResponseStream writable stream and can write bytes incrementally, with Lambda flushing chunks to the client immediately. This is the mechanism that makes SSE transport work over Function URLs.
// src/lambda-mcp-handler.ts
import { streamifyResponse, ResponseStream } from "aws-lambda";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
export const handler = streamifyResponse(
async (event: AWSLambda.APIGatewayProxyEventV2, responseStream: ResponseStream) => {
// Set content-type for SSE
const httpResponseMetadata = {
statusCode: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
};
// Write metadata first (required by streamifyResponse)
responseStream = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata);
const transport = new StreamableHTTPServerTransport({
write: (data: string) => responseStream.write(data),
end: () => responseStream.end(),
});
const server = new Server({ name: "my-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
// ... register tools ...
await server.connect(transport);
await transport.handleRequest(event.body ?? "", event.headers);
}
);
Cold start impact on SSE: the first request to a cold Lambda instance incurs an init duration (typically 300ms–1s for Node.js with the MCP SDK). During init, the SSE connection is not yet established — the client sees latency before the first event. Use Provisioned Concurrency to eliminate cold starts for latency-sensitive MCP deployments.
Concurrency model: one invocation per session
This is the most important architectural difference between Lambda and ECS for MCP hosting. On ECS Fargate, a single task with a Node.js event loop handles hundreds of concurrent SSE sessions through async I/O. On Lambda, each Function URL invocation handles exactly one HTTP request — one MCP session consumes one concurrent Lambda invocation for its entire lifetime.
| Scenario | Lambda concurrency consumed | Risk |
|---|---|---|
| 10 concurrent MCP clients, each with an active SSE connection | 10 | Low — well within default limits |
| 1,000 concurrent MCP clients | 1,000 | Approaches default account limit (1,000); may throttle |
| Tool call takes 5 minutes (e.g., code execution) | 1 for 5 minutes | Long-held invocations reduce available concurrency for other sessions |
| Burst of 500 new sessions in 10 seconds | 500 new invocations requested | Lambda burst limit (3,000/min in most regions) may cause throttling |
# Check current concurrency usage
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name ConcurrentExecutions \
--dimensions Name=FunctionName,Value=McpFunction \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 60 \
--statistics Maximum
# Set reserved concurrency to prevent Lambda from starving other functions
aws lambda put-function-concurrency \
--function-name McpFunction \
--reserved-concurrent-executions 200
If your MCP deployment expects more than a few dozen concurrent sessions, ECS Fargate (one task, many sessions) or Lambda with Provisioned Concurrency (pre-warmed instances, but still one-per-request) is likely the better fit. Lambda Function URLs are ideal for low-to-medium concurrency MCP tools and for developer-facing endpoints where cold starts are tolerable.
Auth patterns for Function URLs
Lambda Function URLs support two auth types: IAM (requires SigV4 request signing — practical only for AWS-native callers) and NONE (public endpoint — implement your own token validation in the handler). Most MCP servers use NONE with application-layer auth.
// Application-layer bearer token validation in the Lambda handler
export const handler = streamifyResponse(async (event, responseStream) => {
const authHeader = event.headers?.authorization ?? "";
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
if (!token || !isValidToken(token)) {
const meta = { statusCode: 401, headers: { "Content-Type": "application/json" } };
awslambda.HttpResponseStream.from(responseStream, meta);
responseStream.write(JSON.stringify({ error: "Unauthorized" }));
responseStream.end();
return;
}
// Proceed with MCP session initialization
});
CORS configuration: set CORS on the Function URL configuration, not inside the handler. The Function URL layer handles OPTIONS preflight responses automatically when CORS is configured — returning a CORS response from inside the handler for a streaming invocation is error-prone because the preflight must complete before the stream begins.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| SSE stream cut at exactly 29 seconds | API Gateway HTTP API in front of Lambda; gateway has a 29-second hard timeout | Switch to Lambda Function URL (authType: NONE, invokeMode: RESPONSE_STREAM) or set up direct Function URL access through CloudFront |
| Response body appears buffered — client receives all SSE events at once at the end | invokeMode is BUFFERED (default) instead of RESPONSE_STREAM | Set invokeMode: lambda.InvokeMode.RESPONSE_STREAM on the Function URL; also verify handler uses streamifyResponse wrapper |
| HTTP 429 throttling during traffic spike | Concurrent sessions hit the reserved concurrency limit or account default limit (1,000) | Request a concurrency limit increase from AWS Support, or set reservedConcurrentExecutions on the function and add client-side retry with backoff |
| CORS preflight fails (OPTIONS returns 403) | CORS not configured on the Function URL; Function URL authType: NONE does not automatically allow OPTIONS | Add cors block to addFunctionUrl with allowedOrigins, allowedHeaders, and allowedMethods including GET and POST |
| Cold start latency of 1–3 seconds on first MCP session | Lambda init duration: loading Node.js runtime, MCP SDK, and handler module from a cold instance | Enable Provisioned Concurrency to keep N instances pre-initialized; or use Lambda Container Images with a larger pre-warmed image |
| Function URL returns HTTP 413 on large MCP tool payloads | Lambda event payload limit is 6 MB for synchronous invocations; streaming mode has a 20 MB request payload limit | For payloads over 6 MB, use streaming mode; for over 20 MB, upload to S3 and pass the S3 key to the tool rather than embedding the raw payload |