Guide · AWS Lambda
MCP Server Lambda Container Images — 10 GB limit, ECR caching, init time vs zip deployment
Lambda container images let you package MCP servers with up to 10 GB of dependencies — overcoming the 250 MB zip limit — but they introduce distinct cold-start characteristics that differ from zip deployments. Three things catch teams off guard when switching to Lambda container images for MCP servers: init time is not proportional to image size (Lambda caches image layers in a regional cache adjacent to the execution environment — a 500 MB image on a warm cache loads as fast as a 50 MB zip, while the same 500 MB image on a cold cache adds 2–10 seconds of pull time to the cold start), ECR image URI must be in the same region as the Lambda function (cross-region ECR pulls are not supported — a us-east-1 Lambda cannot pull from an eu-west-1 ECR repository; replicate images to each region's ECR before deployment), and the Lambda Insights extension adds its own layer (enabling Lambda Insights via the console or CDK injects an extension layer alongside your container image — you must ensure the extension architecture matches your image, e.g., both arm64 or both x86_64).
TL;DR
Use Lambda container images when your MCP server's dependency tree exceeds 250 MB (common with ML inference libraries, native modules, or large SDK bundles) or when you need OS-level customization. Build on AWS base images (public.ecr.aws/lambda/nodejs:22) for Lambda Runtime Interface Client compatibility. Use multi-stage builds to keep the final image lean. Enable arm64 architecture for ~20% lower Lambda cost at equivalent performance for Node.js MCP servers.
Zip vs container image: when to use each
| Factor | Zip deployment | Container image |
|---|---|---|
| Max deployment size | 250 MB (unzipped) | 10 GB (uncompressed image) |
| Cold start (cached layers) | Fastest — no image pull | Near-equivalent if Lambda's image cache is warm |
| Cold start (cache miss) | N/A for zip | 2–10s additional time to pull uncached layers |
| Build pipeline | Simple — npm run build && zip | Docker build + ECR push required |
| OS customization | No — uses Lambda managed runtime | Yes — install system packages, configure locale, etc. |
| Reproducibility | Runtime version managed by AWS | Exact image SHA pinned at deploy time |
| Native modules (Python ML, Rust, C extensions) | Difficult — must compile for Lambda's glibc version | Compile inside the container during build |
| Works with SnapStart | Yes (Java only) | Yes (Java only) |
Most Node.js MCP servers fit in 250 MB zip — the MCP SDK, Zod, and typical tool dependencies are well under this limit. Use container images when you have native addon modules (sharp, canvas, better-sqlite3, bcrypt) that require a specific glibc version, when you're bundling ML model files, or when you need a reproducible OS environment for compliance reasons.
Dockerfile for a Node.js MCP server
# Multi-stage build: install in node:22 (full npm), copy to Lambda base image
# Stage 1: install dependencies
FROM node:22-slim AS builder
WORKDIR /app
COPY package.json package-lock.json ./
# Install only production dependencies
RUN npm ci --omit=dev
COPY tsconfig.json ./
COPY src/ ./src/
# Build TypeScript
RUN npx tsc --outDir dist
# Stage 2: Lambda runtime image
FROM public.ecr.aws/lambda/nodejs:22
# LAMBDA_TASK_ROOT = /var/task
COPY --from=builder /app/dist/ ${LAMBDA_TASK_ROOT}/
COPY --from=builder /app/node_modules/ ${LAMBDA_TASK_ROOT}/node_modules/
# Handler path: file.exportedFunction
# File: /var/task/handler.js, export: handler
CMD [ "handler.handler" ]
Build and push to ECR:
REGION=us-east-1
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REPO=mcp-server
# Authenticate Docker to ECR
aws ecr get-login-password --region $REGION | \
docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
# Create the repository if it doesn't exist
aws ecr create-repository --repository-name $REPO --region $REGION \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE || true # ignore if already exists
# Build for arm64 (Lambda Graviton — 20% cheaper than x86_64)
docker buildx build --platform linux/arm64 \
-t $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO:$(git rev-parse --short HEAD) \
--push .
# Tag as latest (separate tag — keep IMMUTABLE images, just move the pointer)
docker buildx build --platform linux/arm64 \
-t $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO:latest \
--push .
CDK deployment with container image
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as ecr from "aws-cdk-lib/aws-ecr";
import { Duration } from "aws-cdk-lib";
const repo = ecr.Repository.fromRepositoryName(this, "McpRepo", "mcp-server");
const mcpFn = new lambda.DockerImageFunction(this, "McpFunction", {
code: lambda.DockerImageCode.fromEcr(repo, {
// Pin to a specific image digest for reproducibility
// Use tagOrDigest: "sha256:abc123..." in production
tagOrDigest: "latest",
}),
architecture: lambda.Architecture.ARM_64, // Graviton: 20% cheaper
timeout: Duration.minutes(5),
memorySize: 512,
environment: {
NODE_ENV: "production",
MCP_TRANSPORT: "http",
},
});
// Add Function URL for direct HTTP access
mcpFn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
invokeMode: lambda.InvokeMode.RESPONSE_STREAM,
});
// Grant Lambda pull access to the ECR repository
repo.grantPull(mcpFn.role!);
Image layer caching and cold start latency
Lambda maintains an internal image layer cache in each region. When a cold start occurs, Lambda checks if each image layer (identified by its SHA256 digest) is already in the regional cache. Layers shared across multiple Lambda functions and present in the cache are mounted without re-pulling from ECR — typically in under 200ms per layer. The cache is populated on first pull and evicted based on regional LRU policy.
Strategies to maximize cache hit rate:
- Use a stable base image — the AWS base image (
public.ecr.aws/lambda/nodejs:22) is used by thousands of Lambda functions across the region and is almost always in cache. Building on a niche custom base image means a cache miss on the OS layers during cold starts. - Minimize churn in lower layers — put
npm ci --omit=devbeforeCOPY src/in the Dockerfile so the dependency layer only changes whenpackage-lock.jsonchanges, not on every code change. The dependency layer (often the largest) stays cached across deployments. - Avoid
:latesttag in production — use image digest (sha256:...) to ensure the exact layer set is pinned and cache hits are predictable.
| Layer | Cache hit frequency | Pull time (miss) |
|---|---|---|
AWS Lambda base image (public.ecr.aws/lambda/nodejs:22) | Very high — shared across all Node.js Lambdas in region | ~500ms if miss (rare) |
| npm dependencies layer (package-lock.json-driven) | High on patch/minor releases; miss on lock file change | 1–5s depending on layer size |
| Application code layer (src/ changes each deploy) | Miss on every deploy (changes each time) | 100–300ms (small layer) |
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Cold start suddenly 5–10x slower after deploy | New image digest causes a full image cache miss — Lambda pulls all layers from ECR instead of cache | Ensure Dockerfile layer ordering minimizes changes to large layers (move npm ci before COPY src/); verify base image SHA is unchanged |
exec format error on Lambda invoke | Image built for wrong architecture (e.g., linux/amd64 but Lambda function set to ARM_64) | Use docker buildx build --platform linux/arm64 for Graviton Lambda; verify with docker inspect before push |
ResourceNotFoundException: image not found during deployment | ECR image in a different region than the Lambda function | Replicate the ECR image to the Lambda's region; use ECR cross-region replication policy or build + push to each region separately |
| Lambda Insights extension fails with architecture mismatch | Lambda Insights extension layer (x86_64) attached to an arm64 Lambda function | Use the arm64 variant of the Lambda Insights extension ARN, or enable via CloudWatch → Lambda Insights which selects the correct architecture automatically |
| Container image works locally but fails on Lambda with missing shared library | Image built on macOS or with a musl-based image (Alpine) — Lambda uses glibc; musl-compiled binaries are incompatible | Build on public.ecr.aws/lambda/nodejs:22 (Amazon Linux 2023, glibc); avoid Alpine as the build base for native modules |