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

FactorZip deploymentContainer image
Max deployment size250 MB (unzipped)10 GB (uncompressed image)
Cold start (cached layers)Fastest — no image pullNear-equivalent if Lambda's image cache is warm
Cold start (cache miss)N/A for zip2–10s additional time to pull uncached layers
Build pipelineSimple — npm run build && zipDocker build + ECR push required
OS customizationNo — uses Lambda managed runtimeYes — install system packages, configure locale, etc.
ReproducibilityRuntime version managed by AWSExact image SHA pinned at deploy time
Native modules (Python ML, Rust, C extensions)Difficult — must compile for Lambda's glibc versionCompile inside the container during build
Works with SnapStartYes (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:

LayerCache hit frequencyPull 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 change1–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

SymptomCauseFix
Cold start suddenly 5–10x slower after deployNew image digest causes a full image cache miss — Lambda pulls all layers from ECR instead of cacheEnsure 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 invokeImage 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 deploymentECR image in a different region than the Lambda functionReplicate 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 mismatchLambda Insights extension layer (x86_64) attached to an arm64 Lambda functionUse 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 libraryImage built on macOS or with a musl-based image (Alpine) — Lambda uses glibc; musl-compiled binaries are incompatibleBuild on public.ecr.aws/lambda/nodejs:22 (Amazon Linux 2023, glibc); avoid Alpine as the build base for native modules