Guide · AWS Lambda
MCP Server Lambda SnapStart — JVM cold start elimination for Java MCP servers
Lambda SnapStart reduces Java cold starts from 5–10 seconds to ~250ms by taking a memory snapshot after the init phase and restoring it instead of re-initializing from scratch. Three issues trip up Java MCP server deployments using SnapStart: stale network connections in snapshot (database connections, HTTP clients, and gRPC channels opened during init are captured in the snapshot but become stale after restore — you must use CRaC beforeCheckpoint to close them and afterRestore to re-open them), non-deterministic random values (a SecureRandom instance initialized during snapshot has its seed captured — all restored instances start with the same seed state, creating predictable "random" values unless you reseed in afterRestore), and time-dependent state (timestamps, TTL counters, and expiry calculations done in the init phase are frozen at snapshot time and appear to have elapsed by the time the snapshot restores, potentially causing premature cache evictions or expired credentials on first invocation).
TL;DR
Enable SnapStart on a Java 21 (or newer) Lambda function by setting snapStart: SnapStartConf.ON_PUBLISHED_VERSIONS. Implement the CRaC Resource interface with beforeCheckpoint (close DB connections, flush in-flight requests) and afterRestore (re-open connections, reseed SecureRandom, refresh credentials). SnapStart only activates on published versions — $LATEST still cold-starts normally. Restore time is approximately 200–400ms regardless of how long the original init phase took.
How SnapStart works
The standard Lambda lifecycle has three phases: init (load the runtime, load the handler class, run static initializers and the handler constructor), invoke (call the handler for each request), and shutdown. Cold starts are expensive because of the init phase — for a Java MCP server loading Spring, the MCP SDK, JDBC drivers, and schema validation, init can take 5–12 seconds.
SnapStart intercepts after init completes: it fires a beforeCheckpoint event, takes a memory snapshot of the execution environment (JVM heap, class data, file descriptors), and stores it encrypted in a cache. On the next cold start (scaling event or idle timeout recovery), Lambda restores from the snapshot in ~250ms instead of re-running the entire init phase, then fires afterRestore before the first invocation.
| Phase | Without SnapStart | With SnapStart |
|---|---|---|
| Init (class loading, DI container) | 5–12s for Spring / Quarkus | Runs once at publish time; snapshotted |
| Restore (subsequent cold starts) | Repeats full init each time | ~200–400ms snapshot restore |
| afterRestore (reconnect, reseed) | Not applicable | ~50–200ms (your CRaC hooks) |
| Total cold start | 5–12s | ~250–600ms |
| Warm invocation | Same as without SnapStart | Same as without SnapStart |
Enabling SnapStart with CDK
SnapStart is supported on Java 21+ runtimes. Enable it in CDK with snapStart on the function construct. The function must be published as a version — SnapStart does not activate on $LATEST.
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Duration } from "aws-cdk-lib";
const mcpFn = new lambda.Function(this, "McpJavaFunction", {
runtime: lambda.Runtime.JAVA_21,
handler: "com.example.McpHandler::handleRequest",
code: lambda.Code.fromAsset("target/mcp-server.jar"),
timeout: Duration.minutes(5),
memorySize: 1024, // more memory = faster class loading even without SnapStart
snapStart: lambda.SnapStartConf.ON_PUBLISHED_VERSIONS, // enable SnapStart
});
// Publish a version (required for SnapStart to activate)
const version = new lambda.Version(this, "McpVersion", {
lambda: mcpFn,
description: `SnapStart version ${Date.now()}`,
});
// Create alias for Function URL
const alias = new lambda.Alias(this, "McpAlias", {
aliasName: "live",
version,
});
Implementing CRaC hooks for MCP servers
The Coordinated Restore at Checkpoint (CRaC) API lets you register resources that are notified before snapshot (beforeCheckpoint) and after restore (afterRestore). For a Java MCP server, the resources that need CRaC hooks are: database connections, HTTP clients, and any time-seeded state like SecureRandom.
// build.gradle.kts: add CRaC dependency
dependencies {
implementation("io.github.crac:org-crac:0.1.3")
implementation("software.amazon.awssdk:lambda:2.21.0")
implementation("io.modelcontextprotocol:kotlin-sdk:1.0.0")
}
// McpHandler.java
package com.example;
import org.crac.Context;
import org.crac.Core;
import org.crac.Resource;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import java.security.SecureRandom;
public class McpHandler implements Resource {
private SecretsManagerClient secretsClient;
private DatabaseConnectionPool dbPool;
private SecureRandom secureRandom;
public McpHandler() {
// Init phase: set up all resources
this.secretsClient = SecretsManagerClient.create();
this.dbPool = DatabaseConnectionPool.create(loadDbUrl());
this.secureRandom = new SecureRandom();
// Register this instance for CRaC lifecycle events
Core.getGlobalContext().register(this);
}
@Override
public void beforeCheckpoint(Context extends Resource> context) throws Exception {
// Close all network connections before snapshot is taken
// (connections are invalid after restore — stale TCP state)
dbPool.closeAll();
secretsClient.close();
// Note: do NOT reseed secureRandom here — do it in afterRestore
}
@Override
public void afterRestore(Context extends Resource> context) throws Exception {
// Re-establish connections after snapshot restore
secretsClient = SecretsManagerClient.create();
String dbUrl = loadDbUrl(); // re-fetch from Secrets Manager with fresh client
dbPool = DatabaseConnectionPool.create(dbUrl);
// Reseed SecureRandom — critical: snapshot captured seed state is shared
// across all restored instances if not reseeded
secureRandom = new SecureRandom();
secureRandom.nextBytes(new byte[32]); // force entropy seeding
// Refresh any TTL counters or timestamps that were set during init
initTimestamp = System.currentTimeMillis();
}
public String handleRequest(Map<String, Object> event, Context context) {
// Standard MCP request handling
return mcpDispatch(event);
}
}
What you must always do in afterRestore:
- Re-open all network connections (DB pool, HTTP client, gRPC channels)
- Re-fetch short-lived credentials (IAM temporary credentials from instance metadata, Secrets Manager values with rotation TTL shorter than your Lambda idle timeout)
- Reseed
SecureRandomor any PRNG initialized during init - Reset any monotonic counters or timestamps used for TTL or rate limiting
SnapStart limitations for MCP servers
SnapStart is not a complete substitute for Provisioned Concurrency. The restore still takes 200–400ms — if your SLA requires sub-100ms first-response latency, combine SnapStart with Provisioned Concurrency (SnapStart reduces the cost of provisioned instances by making the pre-initialization cheaper at deploy time).
| Limitation | Impact on MCP servers | Mitigation |
|---|---|---|
| Java 21+ runtimes only | Not available for Node.js, Python, or Go MCP servers | Node.js cold starts are 300ms–1s — manageable without SnapStart; use Provisioned Concurrency instead |
| Only activates on published versions | $LATEST cold-starts normally; development and manual testing see full cold starts | Publish versions even in development to test SnapStart behavior |
| Restore time is ~250ms regardless of memory size | Unlike init, larger memory does not reduce restore time | Right-size memory for execution performance, not restore speed |
| Snapshot is tied to one Lambda version | Every new code deployment requires a new snapshot to be taken at version publish time | Expect one cold start per deployment to warm the snapshot; subsequent cold starts use the new snapshot |
| Cannot snapshot open file descriptors or non-heap native state | Native libraries, JNI code, or off-heap buffers may behave unexpectedly after restore | Avoid JNI in the init path; move native initialization to afterRestore |
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Cold starts still 5–10s after enabling SnapStart | Function URL or invocation targeting $LATEST — SnapStart does not activate on $LATEST | Target the published version alias; verify with aws lambda get-alias --function-name McpFunction --name live that alias points to a published version |
Connection refused or Broken pipe on first DB call after restore | DB connection opened during init was captured in snapshot but is invalid after restore (TCP state reset) | Implement beforeCheckpoint to close the pool and afterRestore to re-open it |
| Predictable "random" tokens across restored instances | SecureRandom seed captured in snapshot; all restored instances share the same seed state | Reseed SecureRandom in afterRestore hook; call nextBytes(32) to force entropy collection |
| JWT or credentials expired on first request after restore | Credentials fetched during init have TTL; snapshot is created at a point in time and restored later | Re-fetch credentials in afterRestore instead of caching during init |
| SnapStart snapshot takes too long at deployment time | Init phase is slow; SnapStart must complete init before snapshotting — a 10s init = 10s deployment overhead | Optimize the init path (lazy initialization, smaller dependency set, GraalVM native image for extreme cases) |