Sitemap
AWS Tip

Best AWS, DevOps, Serverless, and more from top Medium writers .

RED Loop: Part 7— Java, What About Cold Start ?

Reading, Experimenting, Documenting

7 min readNov 29, 2025

--

Press enter or click to view image in full size
Photo by NASA on Unsplash

This is the seventh article in the RED (Reading, Experimenting, Documenting) Loop series, inspired by Mark Seemann’s book Code That Fits in Your Head. You can find the previous articles here:

Cold Starts: Why Java Struggles in Serverless

Java performs very well once the JVM is warm and JIT-optimized, but in serverless environments — especially AWS Lambda — the first request triggers a cold start, spinning up the entire application from scratch.

In my “restaurant reservation” example, this took ~9.5 seconds in total, which is far too slow for most business cases.

Press enter or click to view image in full size
CloudWatch logs — start of an app

There are several improvements you can apply when running Java applications on AWS Lambda. The first — and most impactful — is enabling AWS Lambda SnapStart.

What SnapStart Does

AWS Lambda SnapStart (free of cost for Java apps) preloads your Java function once, takes a snapshot of its fully initialized state and then restores that snapshot for near-instant cold starts.

Normally, when a Java Lambda receives its first request, AWS needs to:

  • Start a new JVM
  • Load all classes
  • Run Spring (or any other framework) initialization
  • Create beans, connections, and configuration
  • Initialize clients such as DynamoDB or HTTP clients

This is exactly why Java cold starts are slow.

How SnapStart Fixes the Cold Start Problem

When you publish a new Lambda version, SnapStart:

  1. Starts your function
  2. Runs all initialization logic
  3. Freezes the memory and execution state
  4. Saves that state as a reusable snapshot

Then, for every subsequent invocation, AWS simply restores the snapshot in milliseconds, skipping all the expensive startup steps.

How to Enable SnapStart

You can enable SnapStart directly in the Basic settings of your Lambda function by turning on:

“SnapStart: Published versions”

After that, every time you publish a new Lambda version, AWS will automatically create and use the snapshot.

Press enter or click to view image in full size
Enable SnapStart

After enabling SnapStart, you need to publish a new version of your Lambda function for the snapshot to be created.

Press enter or click to view image in full size
Publish new version

When you publish a new version, make sure that SnapStart is enabled for that version. Only then will AWS create and use the snapshot, ensuring that all cold starts for that version are optimized.

Press enter or click to view image in full size
Published version with SnapStart on

Simplifying API Gateway with Lambda Aliases

When using SnapStart, API Gateway needs to invoke a specific Lambda version. This can be inconvenient in a continuous development workflow, because every time you deploy a new version, you would need to update API Gateway to point to that version.

A better approach is to use a Lambda alias. For example, you can create a dev alias and configure API Gateway to invoke the alias instead of a fixed version. Then, every time you deploy a new version, you only need to update the alias to point to the latest version. API Gateway continues to call the alias, so your integration remains stable and seamless.

Press enter or click to view image in full size
AWS Lambda version alias

To make API Gateway call the alias instead of a fixed version, you need to edit the integration request and add the alias name as a suffix to the Lambda function reference.

Get Roman Novosad’s stories in your inbox

Join Medium for free to get updates from this writer.

For example, if your Lambda function is called code-that-fits and your alias is dev, the integration request should point to:

arn:aws:lambda:REGION:ACCOUNT_ID:function:code-that-fits:dev
Press enter or click to view image in full size
point to the AWS Lambda alias

Don’t forget to deploy the API after making changes to the integration request.

Start with SnapStart Restore

Now, when you call the same /reservation/{id} endpoint, you should notice different logs. This indicates that your Lambda function is being invoked through the alias, and SnapStart is effectively restoring the pre-warmed snapshot, resulting in faster cold starts.

Press enter or click to view image in full size
Start with SnapStart restore feature

Now, we no longer see the Spring Boot initialization phase in the logs — only the restore phase, which takes about 1 second instead of the 5.5 seconds we had at the beginning. That’s a fivefold performance boost just by enabling SnapStart, with no changes to the code!

It’s also worth noting that the more times a cold start occurs, the better the restore cache becomes. In some cases, the restore time can drop even further, down to 0.7 seconds.

The Remaining Problem

Even though the initialization phase is dramatically reduced, the overall execution time for our Lambda function is still around 10 seconds. This doesn’t fully solve the performance problem. The reason is the DynamoDB client initialization and the first query, which still happens after the restore and adds latency.

Further Optimizations: Invoke Priming

One approach to address this is Invoke Priming.

  • This technique involves calling your application endpoints or methods directly during the pre-snapshot hook.
  • Doing so forces the JVM to JIT compile critical code paths during the INIT phase.
  • These compiled methods are then included in the snapshot, so subsequent executions run even faster.

Adding CRaC Support

To implement this, you need to add the CRaC dependency to your project, which enables lifecycle hooks:

        <dependency>
<groupId>org.crac</groupId>
<artifactId>crac</artifactId>
</dependency>

To use SnapStart with Invoke Priming, StreamLambdaHandler should implement the CRaC Resource interface:

 @Override
public void beforeCheckpoint(org.crac.Context<? extends Resource> context) throws Exception {
logger.info("Preparing snapshot (before checkpoint)");
handler.proxy(getAwsProxyRequest(), new MockLambdaContext());
}

@Override
public void afterRestore(org.crac.Context<? extends Resource> context) throws Exception {
logger.info("Snapshot restored (after restore)");
}


private static AwsProxyRequest getAwsProxyRequest () {
final AwsProxyRequest awsProxyRequest = new AwsProxyRequest();
awsProxyRequest.setHttpMethod(HttpMethod.GET.name());
awsProxyRequest.setPath("/reservation/1");

final AwsProxyRequestContext awsProxyRequestContext = new AwsProxyRequestContext();
final ApiGatewayRequestIdentity apiGatewayRequestIdentity= new ApiGatewayRequestIdentity();
apiGatewayRequestIdentity.setApiKey("key");
awsProxyRequestContext.setIdentity(apiGatewayRequestIdentity);
awsProxyRequest.setRequestContext(awsProxyRequestContext);

return awsProxyRequest;
}

beforeCheckpoint()
You send a fake internal HTTP request (/reservation/1) through the Lambda handler. This triggers your Spring Boot application to:

  • initialize all beans
  • load configurations
  • set up routing
  • start the DynamoDB client
  • perform any lazy initialization that normally happens on the first request

In other words, it fully warms up the application before the snapshot is taken. This ensures the restored Lambda is ready instantly.

afterRestore()
Runs after a snapshot is restored. In this example, we just log that the restore happened.

The constructor registers your handler so that it participates in the SnapStart checkpoint and restore process:

public StreamLambdaHandler () {
Core.getGlobalContext().register(this);
}

Improving the Deployment Step in CI/CD

In the previous version of the pipeline, the deployment step simply uploaded the new JAR file to Lambda:

aws lambda update-function-code \
--function-name code-that-fits \
--zip-file "fileb://$JAR_FILE_PATH" \
--publish

This worked, but it had one major limitation: every deployment overwrote the existing function without giving you a stable way to track versions or roll back.

Introducing versioned deployments with aliases

The updated CI/CD step improves this by publishing a new Lambda version on every deployment and automatically pointing an alias to it:

VERSION=$(aws lambda update-function-code ... --publish ...)

AWS returns the exact version number that was created, and the pipeline prints it for visibility.

After that, the workflow updates (or creates, if it doesn’t exist yet) a dev alias:

aws lambda update-alias --name dev --function-version $VERSION

Results

From the CloudWatch logs, we can see that the restore phase now takes about 0.8 seconds and retrieving a reservation takes around 0.7 seconds.

In total, the Lambda execution completes in roughly 1.5 seconds, which is acceptable for most business use cases.

Press enter or click to view image in full size
CloudWatch logs

Summary

In this article, we explored how to reduce Java Spring Boot cold-start times on AWS Lambda. Initially, a cold start took 9.5 seconds, including Spring Boot initialization and DynamoDB access. By enabling SnapStart and using CRaC hooks for warm-up and Invoke Priming, we reduced the total execution time to just 1.5 seconds. This demonstrates how minimal configuration changes and smart use of SnapStart can deliver a 5–6× performance improvement for serverless Java applications, making them viable for real-world business use cases.

If you’d like to learn more about optimizing Java-based applications on AWS Lambda, I highly recommend the articles by Vadym Kazulkin.

I’d like to leave you with a quote from the Code That Fits in Your Head:

Any fool can write the code that a computer can understand. Good programmers write code that humans can understand.

To explore the source code, visit the GitHub repository.

Thank you for reading!

--

--

Roman Novosad
Roman Novosad

Written by Roman Novosad

Software Engineer. AWS | Spring | Apache Kafka Certified. Enjoy observing what is happening around.

AWS Tip
AWS Tip

Published in AWS Tip

Best AWS, DevOps, Serverless, and more from top Medium writers .