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.

PhaseWithout SnapStartWith SnapStart
Init (class loading, DI container)5–12s for Spring / QuarkusRuns 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 start5–12s~250–600ms
Warm invocationSame as without SnapStartSame 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 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 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:

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).

LimitationImpact on MCP serversMitigation
Java 21+ runtimes onlyNot available for Node.js, Python, or Go MCP serversNode.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 startsPublish versions even in development to test SnapStart behavior
Restore time is ~250ms regardless of memory sizeUnlike init, larger memory does not reduce restore timeRight-size memory for execution performance, not restore speed
Snapshot is tied to one Lambda versionEvery new code deployment requires a new snapshot to be taken at version publish timeExpect 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 stateNative libraries, JNI code, or off-heap buffers may behave unexpectedly after restoreAvoid JNI in the init path; move native initialization to afterRestore

Common failure modes

SymptomCauseFix
Cold starts still 5–10s after enabling SnapStartFunction URL or invocation targeting $LATEST — SnapStart does not activate on $LATESTTarget 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 restoreDB 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 instancesSecureRandom seed captured in snapshot; all restored instances share the same seed stateReseed SecureRandom in afterRestore hook; call nextBytes(32) to force entropy collection
JWT or credentials expired on first request after restoreCredentials fetched during init have TTL; snapshot is created at a point in time and restored laterRe-fetch credentials in afterRestore instead of caching during init
SnapStart snapshot takes too long at deployment timeInit phase is slow; SnapStart must complete init before snapshotting — a 10s init = 10s deployment overheadOptimize the init path (lazy initialization, smaller dependency set, GraalVM native image for extreme cases)