DEV Community

Arvind Toorpu
Arvind Toorpu

Posted on

SnapStart Basically Kills Lambda Cold Starts for Java, Python, and .NET. Here's How to Actually Use It

SnapStart Basically Kills Lambda Cold Starts for Java

Cold starts are the one Lambda complaint that never really goes away, especially if you're running Java or a heavier Python stack. SnapStart is AWS's answer, and it's now generally available for Java, Python 3.12+, and .NET 8+. If you haven't touched it yet, this is worth an afternoon.

What SnapStart actually does

Instead of initializing your function's runtime, dependencies, and execution environment from a cold start every time, SnapStart takes a snapshot of a fully initialized execution environment (memory and disk state, post-init) and caches it. When a new instance is needed, Lambda resumes from that snapshot instead of running your init code from scratch.

The practical result: init-phase latency drops by 70 to 90 percent for supported runtimes, according to AWS's own numbers. For a Java function with a heavy Spring context to boot, that's the difference between a multi-second cold start and something that barely registers.

Enabling it

For a supported runtime, it's a one-line change in your function configuration:

aws lambda update-function-configuration \
  --function-name my-java-function \
  --snap-start ApplyOn=PublishedVersions
Enter fullscreen mode Exit fullscreen mode

Note the ApplyOn=PublishedVersions part. SnapStart only applies to published versions of your function, not $LATEST. That trips people up the first time, since testing against $LATEST in the console won't show any SnapStart benefit at all.

Via SAM or CDK, it looks like this (CDK, Python):

from aws_cdk import aws_lambda as lambda_

fn = lambda_.Function(
    self, "MyFunction",
    runtime=lambda_.Runtime.JAVA_21,
    handler="com.example.Handler::handleRequest",
    code=lambda_.Code.from_asset("build/function.zip"),
    snap_start=lambda_.SnapStartConf.ON_PUBLISHED_VERSIONS,
)
Enter fullscreen mode Exit fullscreen mode

The gotcha nobody warns you about: uniqueness

Snapshots capture your execution environment's state, including anything you initialized before the handler runs, like a random seed, a cached secret, or a database connection. If your init code generates something that's supposed to be unique per execution environment (a UUID used as an instance identifier, for example) and you generate it once outside the handler, every resumed snapshot will have the exact same value, because they're all resuming from the identical frozen state.

AWS's guidance here is specific: anything that needs to be unique per invocation or per environment needs to be regenerated inside the handler, or you need to use the Lambda runtime hooks (beforeCheckpoint and afterRestore for Java) to refresh things like connections and credentials on resume rather than assuming init-time state stays valid forever.

// Java: refreshing a connection on restore using runtime hooks
import com.amazonaws.services.lambda.runtime.CoreLifecycleHooks;

public class Handler {
    private Connection dbConnection;

    public Handler() {
        CoreLifecycleHooks.registerBeforeCheckpoint(this::beforeCheckpoint);
        CoreLifecycleHooks.registerAfterRestore(this::afterRestore);
    }

    private void afterRestore() {
        // re-establish anything that shouldn't be shared across resumed snapshots
        this.dbConnection = createFreshConnection();
    }
}
Enter fullscreen mode Exit fullscreen mode

Java 25 made this better

Java 25 on Lambda changed how tiered compilation interacts with SnapStart and Provisioned Concurrency. Previously, SnapStart would stop compilation optimization at tier C1. Starting with Java 25, that cap is gone, so functions get the benefit of full JIT optimization even when running from a SnapStart snapshot, instead of being stuck at a less-optimized compilation tier indefinitely.

If you're on an older Java version specifically because you assumed SnapStart's compilation ceiling was a permanent limitation, it's worth re-checking against Java 25.

What's NOT supported

Worth knowing before you go looking for it:

  • Container image functions don't support SnapStart, only zip-based deployments on supported managed runtimes.
  • Node.js and Ruby runtimes aren't supported as of this writing; only Java 11+, Python 3.12+, and .NET 8+.
  • OS-only (provided.al2/al2023) runtimes aren't supported either.

Pairing it with Graviton

SnapStart cuts init latency. Graviton (arm64) instances cut execution cost and generally improve throughput for CPU-bound workloads. They're independent levers, and stacking both together is a reasonable default for any new Java or Python Lambda function where cold start and cost both matter:

# SAM template snippet
Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: java21
      Architectures:
        - arm64
      SnapStart:
        ApplyOn: PublishedVersions
Enter fullscreen mode Exit fullscreen mode

Should you turn this on everywhere

For Java functions specifically, yes, almost by default, given how much init-phase latency Java pays compared to something like Node. For Python and .NET, it's still worth it if you're seeing meaningful cold start impact on user-facing latency, but the win is smaller since those runtimes generally cold-start faster to begin with.

The one thing I'd actually test before flipping it on for a production function: run your function through a few resume cycles and check for any state that quietly stayed frozen from the snapshot when you expected it to be fresh. That's the failure mode that doesn't show up in a quick smoke test, only in production traffic days or weeks later.

Further reading

Top comments (0)