Guide · AWS Lambda
MCP Server Lambda Provisioned Concurrency — eliminate cold starts, keep MCP sessions warm
Lambda Provisioned Concurrency pre-initializes N Lambda execution environments so the first request hits a warm instance with zero init duration. Three things cause cold-start pain in Lambda-hosted MCP servers: Node.js module load time (the MCP SDK, Zod validation schemas, database clients, and application code together can take 300ms–1s to load from a cold instance), VPC attachment latency (if the Lambda function is in a VPC, attaching the elastic network interface adds another 500ms–3s to cold start — use VPC endpoints and keep the Lambda outside the VPC when possible), and traffic bursts after low-traffic periods (agent workflows that spike at the start of a business day or CI pipeline run create bursts of cold starts that all arrive simultaneously, causing cascading init latency for the first wave of users).
TL;DR
Set Provisioned Concurrency on a Lambda alias (not $LATEST). Use Application Auto Scaling with a TargetTrackingScalingPolicy on LambdaProvisionedConcurrencyUtilization at 0.7 target to keep headroom before on-demand cold starts begin. Cost: provisioned instances are always-on — ~$0.000015/GB-second for provisioned duration (vs $0.0000166667 for on-demand). At 24/7 usage with ≥2 instances, ECS Fargate on Spot becomes cheaper.
What Provisioned Concurrency does and does not do
Provisioned Concurrency initializes N execution environments before any request arrives, running the function's initialization code (module imports, SDK setup, DB connection creation). When a request arrives, the environment is ready — no init duration. What it does not do: it does not change the one-invocation-per-request model, does not remove the function timeout, and does not reduce invocation cost — you pay the provisioned-duration rate (always-on) plus the standard invocation fee for each request.
| Condition | Without Provisioned Concurrency | With Provisioned Concurrency |
|---|---|---|
| First request after 15-min idle | Cold start: 300ms–1s init + execution time | Warm: execution time only (no init) |
| Burst of 5 simultaneous new sessions | 5 cold starts if no warm instances available | 0 cold starts if provisioned ≥ 5; excess uses on-demand (may cold-start) |
| Sustained traffic at 50 concurrent sessions | Occasional cold starts as Lambda scales out | No cold starts up to provisioned count; above that, on-demand instances (may cold-start) |
| Function deployment (new code version) | New $LATEST immediately served; cold starts on first requests | Must re-provision on the new alias after deployment; old provisioned instances drain |
Configuring Provisioned Concurrency with CDK
Provisioned Concurrency must be set on a Lambda alias, not on $LATEST. The CDK pattern: publish a Lambda version, create an alias pointing to it, then apply provisioned concurrency on the alias. Application Auto Scaling manages the count dynamically.
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as lambdaNodeJs from "aws-cdk-lib/aws-lambda-nodejs";
import * as appscaling from "aws-cdk-lib/aws-applicationautoscaling";
import { Duration } from "aws-cdk-lib";
const mcpFn = new lambdaNodeJs.NodejsFunction(this, "McpFunction", {
entry: "src/lambda-mcp-handler.ts",
runtime: lambda.Runtime.NODEJS_22_X,
timeout: Duration.minutes(5),
memorySize: 512,
});
// Publish a version (required for provisioned concurrency)
const version = mcpFn.currentVersion;
// Create an alias pointing to the published version
const alias = new lambda.Alias(this, "McpAlias", {
aliasName: "live",
version,
// Static provisioned concurrency (no auto scaling)
provisionedConcurrentExecutions: 2,
});
// Attach Function URL to the alias (not $LATEST)
alias.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
invokeMode: lambda.InvokeMode.RESPONSE_STREAM,
});
// Auto Scaling: scale provisioned concurrency between 2 and 20
const target = new appscaling.ScalableTarget(this, "ScalableTarget", {
serviceNamespace: appscaling.ServiceNamespace.LAMBDA,
resourceId: `function:${mcpFn.functionName}:live`,
scalableDimension: "lambda:function:ProvisionedConcurrency",
minCapacity: 2,
maxCapacity: 20,
});
target.scaleToTrackMetric("PcuUtilizationTracking", {
targetValue: 0.7, // scale out when 70% of provisioned instances are in use
predefinedMetric: appscaling.PredefinedMetric.LAMBDA_PROVISIONED_CONCURRENCY_UTILIZATION,
scaleInCooldown: Duration.minutes(5),
scaleOutCooldown: Duration.seconds(30),
});
Deployment pattern for zero-downtime updates: when deploying new code, publish a new Lambda version, update the alias to point to it (CDK does this automatically via currentVersion), and wait for the new provisioned concurrency to warm up before traffic shifts. Use Lambda weighted aliases to shift 10% → 100% if you want canary-style rollout. See MCP Server Canary Deployment for the pattern.
Cost model: provisioned vs on-demand vs ECS Fargate
Provisioned Concurrency charges for initialization compute time at a slightly lower rate than on-demand — but the charge is continuous, not per-request. This makes it more expensive than on-demand at low traffic but can be cost-neutral at sustained load.
| Pricing component | On-demand Lambda | Provisioned Concurrency |
|---|---|---|
| Init duration charge | $0.0000166667 / GB-second | $0.000015 / GB-second (provisioned rate, always-on) |
| Execution duration charge | $0.0000166667 / GB-second | $0.0000097 / GB-second (lower provisioned rate) |
| Request charge | $0.20 / million | $0.20 / million (same) |
| Idle cost (no traffic) | $0 (no invocations = no charge) | Provisioned instances billed even at zero traffic |
ECS Fargate Spot break-even: a single Fargate Spot task at 0.5 vCPU / 1 GB costs ~$0.0102/hr (~$7.35/month). A single Lambda provisioned concurrency instance at 512 MB running 24/7 costs ~$0.000015 × 0.5 GB × 86,400 s/day × 30 days ≈ $19.44/month. At sustained 24/7 load needing ≥2 warm instances, ECS Fargate Spot on a single task is typically cheaper — Lambda Provisioned Concurrency is cost-effective for traffic that is bursty (a few hours/day) rather than continuous.
Monitoring provisioned concurrency utilization
AWS publishes two CloudWatch metrics for provisioned concurrency: ProvisionedConcurrencyUtilization (percentage of provisioned instances currently in use) and ProvisionedConcurrencySpilloverInvocations (invocations that overflowed provisioned capacity and hit on-demand cold start). Watch both.
# Check utilization — alert if consistently above 80% (scale-out may lag)
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name ProvisionedConcurrencyUtilization \
--dimensions Name=FunctionName,Value=McpFunction Name=Resource,Value=McpFunction:live \
--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
# Spillover invocations = cold starts happening despite provisioned concurrency
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name ProvisionedConcurrencySpilloverInvocations \
--dimensions Name=FunctionName,Value=McpFunction Name=Resource,Value=McpFunction:live \
--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 Sum
If ProvisionedConcurrencySpilloverInvocations is non-zero, your auto-scaling policy is too slow to respond to traffic spikes. Lower scaleOutCooldown (already at 30 seconds is aggressive), increase minCapacity, or switch to a scheduled scaling action that pre-scales before known traffic spikes (e.g., business-hour start times).
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Provisioned Concurrency set but cold starts still happen | Function URL or invoke is targeting $LATEST, not the alias with provisioned concurrency configured | Verify the Function URL is attached to the alias (:live), not the function ARN without alias qualifier |
| New deployment causes cold starts despite provisioned concurrency | After version publish, new provisioned instances must warm up before traffic shifts; old alias still serves old version during warm-up | Use CDK deployment with blue/green alias shift; monitor ProvisionedConcurrencyInitializations metric before updating alias weight |
| Auto Scaling does not scale out fast enough during burst | Auto Scaling scaleOutCooldown and the CloudWatch metric evaluation period introduce latency | For predictable bursts, use scheduled scaling actions (target.scaleOnSchedule) to pre-scale before expected traffic ramps |
| Cost unexpectedly high | Provisioned instances billed 24/7 even during low-traffic hours | Use scheduled scale-in to reduce provisioned count during off-hours; or switch to ECS Fargate Spot for sustained 24/7 load |
TooManyRequestsException: Provisioned concurrency is being updated | Concurrent CDK deployments or auto-scaling actions conflict during provisioned concurrency update | Serialize deployments; allow provisioned concurrency update to complete before triggering another (check ProvisionedConcurrencyStatus = READY before proceeding) |