Pavan Rangani

HomeBlogAWS Lambda SnapStart: Eliminating Cold Starts for Java Functions

AWS Lambda SnapStart: Eliminating Cold Starts for Java Functions

By Pavan Rangani · April 7, 2026 · Cloud Management

AWS Lambda SnapStart: Eliminating Cold Starts for Java Functions

AWS Lambda SnapStart: Zero Cold Starts for Java

AWS Lambda SnapStart is a feature that reduces Java function cold start times from several seconds to roughly 200 milliseconds. It works by taking a snapshot of the initialized function after the init phase completes, then restoring from that snapshot for subsequent invocations. As a result, Java developers can finally use Lambda for latency-sensitive workloads without the cold start penalty that long made serverless Java impractical. The mechanism is worth understanding in detail, because the gains only materialize if you respect what can and cannot be safely snapshotted.

Cold starts have historically been Java’s biggest weakness in serverless environments. The JVM initialization, class loading, and framework startup — Spring Boot especially — consume several seconds on first invocation. Moreover, that penalty recurs whenever Lambda scales up or recovers from idle timeout, so it is not a one-time cost you can ignore. Consequently, many teams avoided Java for Lambda despite Java’s strong throughput once warm. SnapStart shifts that init cost from request time to deployment time, which changes the calculus entirely.

How SnapStart Works Under the Hood

SnapStart leverages the Firecracker microVM’s snapshot and restore capability. During deployment, Lambda runs your function’s init phase, takes an encrypted memory snapshot (built on CRaC, Coordinated Restore at Checkpoint), and stores it. When a new execution environment is needed, Lambda restores from that snapshot instead of running init from scratch. Therefore the entire JVM state — loaded classes, JIT-warmed code paths, initialized beans, and connection pool scaffolding — becomes immediately available. To keep restores fast even under sudden scale-up, Lambda caches snapshot chunks and loads memory pages lazily as the function touches them.

# AWS SAM template with SnapStart enabled
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  OrderFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.myapp.OrderHandler::handleRequest
      Runtime: java21
      MemorySize: 1024
      Timeout: 30
      SnapStart:
        ApplyOn: PublishedVersions  # Enable SnapStart
      AutoPublishAlias: live        # Required for SnapStart
      Environment:
        Variables:
          DB_ENDPOINT: !Ref DatabaseEndpoint
          CACHE_ENDPOINT: !Ref CacheEndpoint
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref OrdersTable
AWS Lambda SnapStart serverless infrastructure
SnapStart takes a memory snapshot after init, restoring it for near-instant cold starts

Two configuration details are mandatory and frequently missed. SnapStart only applies to published versions, not the $LATEST alias, so AutoPublishAlias (or an explicit version-and-alias setup) is required. And because the snapshot is taken at publish time, deployments take longer — you are trading a slower deploy for a far faster restore. That is almost always the right trade for user-facing APIs.

Lifecycle Hooks: beforeCheckpoint and afterRestore

Some resources cannot be safely snapshotted. Network connections go stale, random number generators would replay identical sequences, and temporary credentials expire between snapshot and restore. SnapStart exposes lifecycle hooks through the CRaC API to handle these cases. Specifically, beforeCheckpoint lets you release resources before the snapshot, while afterRestore re-initializes them — and re-establishes anything that must be fresh — after restore.

import org.crac.Context;
import org.crac.Core;
import org.crac.Resource;

public class OrderHandler implements RequestHandler<APIGatewayProxyRequestEvent,
        APIGatewayProxyResponseEvent>, Resource {

    private DynamoDbClient dynamoDb;
    private ObjectMapper mapper;
    private HikariDataSource dataSource;

    public OrderHandler() {
        // Init phase — this gets snapshotted
        Core.getGlobalContext().register(this);
        this.mapper = new ObjectMapper();
        this.dynamoDb = DynamoDbClient.create();
        initializeConnectionPool();
    }

    @Override
    public void beforeCheckpoint(Context<? extends Resource> context) {
        // Release anything tied to a live socket or credential
        if (dataSource != null) dataSource.close();
        dynamoDb.close();
    }

    @Override
    public void afterRestore(Context<? extends Resource> context) {
        // Re-create clients and pools against the new environment
        this.dynamoDb = DynamoDbClient.create();
        initializeConnectionPool();
        refreshSecrets(); // pull fresh, unexpired credentials
    }

    private void initializeConnectionPool() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(System.getenv("DB_ENDPOINT"));
        config.setMaximumPoolSize(5);
        config.setMinimumIdle(1);
        this.dataSource = new HikariDataSource(config);
    }

    @Override
    public APIGatewayProxyResponseEvent handleRequest(
            APIGatewayProxyRequestEvent event, Context context) {
        Order order = mapper.readValue(event.getBody(), Order.class);
        orderRepository.save(order);
        return new APIGatewayProxyResponseEvent()
            .withStatusCode(200)
            .withBody(mapper.writeValueAsString(order));
    }
}

Framework users get help here. Spring Boot 3.2+ integrates with CRaC, so registering beans as CRaC Resources or relying on Spring’s lifecycle support automates much of the connection handling. Even so, you remain responsible for application-specific state — anything you cached during init that must be unique or time-sensitive per environment still needs an afterRestore refresh.

Performance Benchmarks

Published benchmarks show dramatic improvements. A Spring Boot 3 Lambda that previously had six-to-eight-second cold starts restores in roughly 150 to 200 milliseconds. Notably, restore time stays consistent regardless of how many beans the application wires, because the work of constructing them already happened at snapshot time. That consistency is what makes Java competitive with Go and Python on cold-start latency for the first time.

// Cold Start Benchmarks (1024MB memory, Java 21) — representative
// Without SnapStart:
//   Init duration: ~6,200ms (Spring Boot 3.2)
//   Init duration: ~3,800ms (Micronaut 4)
//   Init duration: ~2,100ms (plain Java + SDK)

// With SnapStart:
//   Restore duration: ~180ms (Spring Boot 3.2)
//   Restore duration: ~150ms (Micronaut 4)
//   Restore duration: ~120ms (plain Java + SDK)

// Warm invocation (both):
//   Duration: 15-50ms (depends on business logic)
Lambda performance benchmarks dashboard
SnapStart reduces Java cold starts from 6+ seconds to under 200ms consistently

Priming: Squeezing Out the First-Invocation Tax

There is a subtlety the headline numbers hide. SnapStart preserves loaded classes, but lazily initialized code paths — the first JSON deserialization, the first JDBC round trip, the first TLS handshake — may still pay a one-time cost on the initial real request after restore. The recommended remedy is priming: exercise the hot path inside the constructor or a beforeCheckpoint hook so the relevant classes load and the JIT warms before the snapshot is captured.

public OrderHandler() {
    Core.getGlobalContext().register(this);
    this.mapper = new ObjectMapper();
    this.dynamoDb = DynamoDbClient.create();

    // Prime serialization so the first real request doesn't pay for it
    try {
        Order sample = new Order("PRIME", 0);
        mapper.readValue(mapper.writeValueAsString(sample), Order.class);
    } catch (Exception ignored) { /* warm-up only */ }
}

Priming is the difference between a “200ms restore but 900ms first request” and a genuinely uniform sub-300ms experience. For latency-SLO-bound endpoints, it is therefore usually worth the effort. Be selective, though: priming everything inflates the snapshot and lengthens deploys, so concentrate on the genuinely hot paths — the serializer, the primary datastore client, and the one or two routes that carry the bulk of traffic. A practical heuristic is to prime whatever your p99 latency budget cannot afford to absorb on a single cold request.

Common Pitfalls and the Uniqueness Trap

The most dangerous SnapStart bug is silent. Because every restored environment starts from one identical snapshot, anything generated during init is shared across all of them. If your init phase produces a UUID, seeds a Random, or caches a timestamp, every concurrent instance will reuse those exact values — a correctness and security hazard. Cached DNS resolutions and TLS sessions can likewise be stale after restore.

  • Replace Random with SecureRandom, and re-seed it in afterRestore
  • Never cache temporary STS credentials in init; fetch them lazily or refresh on restore
  • Close and reopen network connections and pools in the lifecycle hooks
  • Generate unique IDs at request time, not init time
  • Test locally and monitor RestoreDuration in CloudWatch Metrics

When NOT to Use SnapStart: Trade-offs

SnapStart is not free of cost or limitation. It is most valuable for Java Lambda functions where cold-start latency directly affects users — API endpoints, synchronous event processors, and interactive services. By contrast, it adds little for async, latency-tolerant workloads such as SQS or S3 event processors, where an occasional multi-second start is harmless. There are also constraints to weigh: SnapStart historically did not apply to functions configured with provisioned concurrency (the two solve overlapping problems differently), it lengthens deployment time, and functions that rely on ephemeral uniqueness require careful CRaC wiring to remain correct. If your function is rarely cold, or its dependencies resist snapshotting, the engineering effort may outweigh the benefit. As always, confirm current runtime support and limits in the official documentation, since AWS expands SnapStart’s coverage over time. See the AWS Lambda SnapStart documentation for configuration details and supported runtimes.

Serverless architecture decision guide
SnapStart makes Java viable for latency-sensitive serverless workloads

In conclusion, AWS Lambda SnapStart removes the last major barrier to running Java in serverless environments — provided you handle uniqueness, prime the hot path, and reserve it for the latency-sensitive functions that actually benefit. With sub-300ms restores, well-tuned Java functions match the startup feel of interpreted languages while keeping Java’s runtime throughput. Enable it on published versions, implement the CRaC lifecycle hooks deliberately, and measure restore duration in production to confirm the gains hold for your workload.

← Back to all articles