AWS Lambda SnapStart for Coding-Agent Tools: Faster Cold Starts on Java, Python and .NET

17 views
A coding agent calls tools dozens of times per task: parse a file, apply a patch, run a linter, query a repository index. When those tools run in Lambda and a burst of work arrives, new execution environments have to start. For a Java tool that loads a large framework, or a Python tool that imports heavy parsing libraries, that startup (the cold start) can take seconds. The agent looks stuck, even though the model is fine. Lambda SnapStart moves that startup work to deployment time. When you publish a function version, Lambda runs your initialization code once, takes a snapshot of the initialized environment, and later resumes new environments from that snapshot instead of starting from scratch. This guide explains how it works, which runtimes support it, how to write code that stays correct after a restore, what it costs, and how to measure whether it helped. It replaces our earlier Java-only SnapStart post.
TL;DR
  • SnapStart supports Java 11+, Python 3.12+ and .NET 8+ managed runtimes. Node.js, Ruby and OS-only runtimes aren’t supported.
  • It works only on published versions (and aliases that point to them), never on $LATEST.
  • Anything generated during init (IDs, seeds, cached credentials, open connections) is shared by every restored environment. Generate unique values in the handler or an after-restore hook.
  • Java: no extra charge. Python and .NET: you pay for snapshot caching (per published version, minimum 3 hours) and for each restore.
  • SnapStart can’t be combined with provisioned concurrency, EFS, or ephemeral storage above 512 MB.

How SnapStart works

A normal cold start has an Init phase (load code, start the runtime, run your initialization code) before the first Invoke. With SnapStart, Lambda runs Init when you publish a version. It then takes a Firecracker microVM snapshot of the memory and disk state, encrypts it and caches it. When the version is first invoked, and whenever it scales up, Lambda restores new environments from that snapshot. Some details from the Lambda documentation that shape how you use it:
  • Init at publish time can run for up to 15 minutes (the higher of 130 seconds or the function timeout). Before-snapshot hooks count toward that limit.
  • On restore, the runtime must load and after-restore hooks must finish within 10 seconds, or the invocation fails with SnapStartTimeoutException.
  • Lambda keeps several copies of each snapshot and patches them with runtime and security updates automatically.
  • For Java, Lambda deletes a snapshot after 14 days without invocations. The next call returns SnapStartNotReadyException while Lambda builds a new one.
  • SnapStart works best at scale. Functions invoked rarely might not see the same improvement.
Figure 1. SnapStart moves initialization to publish time; new execution environments resume from the cached snapshot. Diagram by CheatCoders.
Figure 1. SnapStart moves initialization to publish time; new execution environments resume from the cached snapshot. Diagram by CheatCoders.

SnapStart, provisioned concurrency or neither?

OptionStartupCost modelGood fit for agent tools
On-demand (no SnapStart)Full Init on each new environmentRequests + durationLight tools, Node.js tools, rarely used tools
SnapStartRestore from snapshot; can be sub-second in good casesJava: no extra charge. Python/.NET: caching + restore chargesHeavy-init Java, Python or .NET tools with bursty traffic
Provisioned concurrencyPre-initialized; designed for double-digit millisecond responseExtra charge for configured capacityTools with steady traffic and strict latency targets
The Lambda docs suggest provisioned concurrency when your cold start requirements are stricter than SnapStart can meet. You can’t use both on the same function version. A sensible split for coding agents: keep the thin model gateway (often Node.js, which SnapStart doesn’t support) as a regular function, and turn on SnapStart for heavy tool runners.

Before you start

  • A tool function on a supported runtime, in a Region that supports SnapStart (all commercial Regions except Asia Pacific (New Zealand) and Asia Pacific (Taipei)).
  • No EFS mount and ephemeral storage of 512 MB or less.
  • Callers (Step Functions, your agent runtime) that can invoke an alias such as live instead of the bare function name.

Step 1: Turn on SnapStart and publish a version

bash
aws lambda update-function-configuration \
  --function-name agent-parse-tool \
  --snap-start ApplyOn=PublishedVersions

aws lambda wait function-updated --function-name agent-parse-tool

VERSION=$(aws lambda publish-version --function-name agent-parse-tool \
  --query Version --output text)

# SnapStart is ready when OptimizationStatus is On and State is Active
aws lambda get-function-configuration \
  --function-name agent-parse-tool --qualifier "$VERSION" \
  --query '{snap: SnapStart, state: State}'

aws lambda update-alias --function-name agent-parse-tool \
  --name live --function-version "$VERSION"
While a version is Pending, Lambda is running Init and taking the snapshot, and invocations of that version fail. Move the alias only after the version is Active. With AWS SAM, the same setup is two properties:
yaml
Resources:
  ParseTool:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: agent-parse-tool
      Runtime: python3.13
      Handler: app.handler
      MemorySize: 2048
      Timeout: 60
      SnapStart:
        ApplyOn: PublishedVersions
      AutoPublishAlias: live
AutoPublishAlias publishes a new version whenever the code changes and points live at it. Every published version gets its own snapshot, and for Python and .NET each snapshot carries a caching charge for as long as the version exists, so clean up old versions.

Step 2: Write restore-safe Python

Do the expensive work during init: imports, SDK clients, loading grammars or models. Python’s runtime includes the Snapshot Restore for Python library, which gives you two hooks: @register_before_snapshot and @register_after_restore.
python
import json
import os
import random
import uuid

import boto3
from snapshot_restore_py import register_after_restore, register_before_snapshot

# Heavy, deterministic setup belongs in init: it ends up in the snapshot.
s3 = boto3.client("s3")
GRAMMAR_DIR = "/tmp/grammars"
ENV_ID = None  # unique per execution environment; set after restore


@register_before_snapshot
def preload_grammars():
    """Download parser grammars once, before the snapshot is taken."""
    os.makedirs(GRAMMAR_DIR, exist_ok=True)
    for name in ("python.so", "typescript.so"):
        s3.download_file("agent-tool-assets", f"grammars/{name}", f"{GRAMMAR_DIR}/{name}")


@register_after_restore
def make_unique():
    """Runs in every restored environment, before the first invocation."""
    global ENV_ID
    ENV_ID = uuid.uuid4().hex  # uuid4 reads fresh OS entropy


rng = random.SystemRandom()  # CSPRNG: safe across restores


def handler(event, context):
    tool_call_id = event["tool_call_id"]  # idempotency key comes from the caller
    sample = rng.random() < 0.01          # e.g. 1% detailed tracing
    result = parse_file(event["repo"], event["path"], grammar_dir=GRAMMAR_DIR)
    print(json.dumps({"tool_call_id": tool_call_id, "env": ENV_ID, "trace": sample}))
    return {"tool_call_id": tool_call_id, "symbols": result}
Three rules keep this correct:
  • No unique values in init. A UUID, seed or timestamp created at init time is identical in every restored environment. Create it in the handler or in an after-restore hook.
  • Use a CSPRNG. After a restore, Lambda reseeds the kernel’s /dev/random and /dev/urandom. Generators that read from them, such as Python’s random.SystemRandom or Java’s SecureRandom, stay random. A random.seed() call during init does not.
  • Don’t trust in-memory state for idempotency. Use the tool_call_id from the orchestrator and a durable ledger (for example DynamoDB conditional writes), never a set in process memory.

Step 3: Java tools use CRaC hooks

For Java, runtime hooks come from the open-source CRaC project (org.crac). Add the org.crac:crac dependency, implement Resource, and register it with the global context. Keep a strong reference to the resource: the context holds only a weak reference, and a garbage-collected resource’s hooks don’t run.
java
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import java.util.Map;
import java.util.UUID;
import org.crac.Core;
import org.crac.Resource;

public class ApplyPatchHandler implements RequestHandler<Map<String, String>, Map<String, String>>, Resource {

    private static final PatchEngine ENGINE = PatchEngine.loadGrammars(); // heavy init, snapshotted
    private String environmentId;

    public ApplyPatchHandler() {
        Core.getGlobalContext().register(this); // same registration pattern as the AWS docs example
    }

    @Override
    public void beforeCheckpoint(org.crac.Context<? extends Resource> context) {
        ENGINE.warmUp(); // optional: exercise code paths so classes are loaded before the snapshot
    }

    @Override
    public void afterRestore(org.crac.Context<? extends Resource> context) {
        environmentId = UUID.randomUUID().toString(); // unique per restored environment
    }

    @Override
    public Map<String, String> handleRequest(Map<String, String> event, Context ctx) {
        String toolCallId = event.get("tool_call_id");
        String status = ENGINE.applyOnce(toolCallId, event.get("patch"));
        return Map.of("tool_call_id", toolCallId, "status", status, "env", environmentId);
    }
}
The documentation’s Java guidance also suggests preloading classes during init, even with a dummy request to your framework, so class loading happens before the snapshot rather than on the first real call. For Java, Lambda also provides an open-source SnapStart scanning tool that flags code that assumes uniqueness.

Step 4: Network connections and credentials

Connections opened during init aren’t guaranteed to work after a restore. AWS SDK connections usually resume automatically; for others (database pools, Redis clients, custom HTTP keep-alive), validate and reconnect in the handler or in an after-restore hook. Credentials are handled for you: with SnapStart, the runtime uses container credentials instead of access-key environment variables, so they don’t expire inside the snapshot. Do refresh any other temporary data, such as tokens or cached timestamps, before using it.

Step 5: Measure before you claim victory

For SnapStart functions, the REPORT log line on a new environment includes Restore Duration and Billed Restore Duration, and the cold start is Restore Duration plus Duration. This Logs Insights query, adapted from the Lambda docs, gives cold and warm percentiles side by side:
text
filter @type = "REPORT"
| parse @log /\d+:\/aws\/lambda\/(?<function>.*)/
| parse @message /Restore Duration: (?<restoreDuration>.*?) ms/
| stats count(*) as invocations,
    pct(@duration + coalesce(@initDuration, 0) + coalesce(restoreDuration, 0), 50) as p50,
    pct(@duration + coalesce(@initDuration, 0) + coalesce(restoreDuration, 0), 99) as p99
  group by function, (ispresent(@initDuration) or ispresent(restoreDuration)) as coldstart
| sort by coldstart desc
Run it for a week before enabling SnapStart and a week after, on the same tool and memory size. X-Ray shows a Restore subsegment instead of Initialization for SnapStart functions. Note that AWS_LAMBDA_LOG_GROUP_NAME and AWS_LAMBDA_LOG_STREAM_NAME aren’t available in SnapStart functions, so don’t build log links from them.

Worked example: a Python repository-parsing tool

  1. The agent’s parse_symbols tool imports a parsing library and loads grammar files for several languages. Logs show long Init Duration values whenever a burst of agent sessions scales the function out.
  2. You move grammar downloads into @register_before_snapshot, keep imports and the S3 client at module level, and move the per-environment ID into @register_after_restore.
  3. You enable SnapStart, publish version 12, wait for Active, and point live at it. Step Functions already invokes agent-parse-tool:live, so nothing else changes.
  4. The Logs Insights query now shows Restore Duration on cold starts instead of Init Duration. Compare the cold-start p99 with last week’s to decide whether the caching and restore charges are worth it for this tool.
  5. CI publishes only on release, not on every commit, and a cleanup job deletes versions older than the previous two so you don’t pay to cache snapshots nobody calls.

Troubleshooting

SymptomLikely causeFix
No change in cold startsCallers invoke $LATEST or the unqualified nameInvoke the alias or a version ARN
Invocations fail right after publishingVersion still Pending while the snapshot is createdWait for State: Active before moving the alias
SnapStartTimeoutExceptionRuntime load plus after-restore hooks took longer than 10 secondsMove work to init or the handler; keep hooks small
SnapStartNotReadyException on a Java toolNo invocations for 14 days, so the snapshot was deletedRetry after the version is Active; keep a light schedule if needed
Duplicate IDs across environmentsIDs or seeds created during initGenerate them in the handler or an after-restore hook; use a CSPRNG
Intermittent connection errors after a restoreNon-SDK connections opened during initValidate and reconnect after restore
Can’t enable SnapStartUnsupported runtime, provisioned concurrency, EFS or ephemeral storage above 512 MBRemove the conflicting setting or keep the function on-demand

Production checklist

  • ☐ SnapStart only on heavy-init tools with a supported runtime
  • ☐ Callers invoke an alias; CI moves it only after the version is Active
  • ☐ No unique values, seeds or timestamps generated during init
  • ☐ CSPRNG for anything random; idempotency keyed by tool_call_id in durable storage
  • ☐ Non-SDK connections revalidated after restore
  • ☐ Restore and cold-start percentiles tracked before and after the change
  • ☐ Old versions cleaned up (Python and .NET pay per cached snapshot)

FAQ

Does SnapStart need code changes?

Often none, according to AWS. You do need changes if init creates unique values, opens non-SDK connections, or caches temporary data. Those are the cases the hooks are for.

How fast is a restore?

AWS describes startup as low as sub-second in good cases, and it depends on your code, memory size and traffic. Measure with the query above rather than relying on published benchmarks, including ones from older versions of this post.

Can I use SnapStart for a Node.js model gateway?

No. Node.js isn’t a supported runtime. Node.js cold starts are usually smaller anyway; see our Lambda cold start guide for Node.js techniques.

Is SnapStart free?

For Java managed runtimes there’s no additional charge. For Python and .NET there are caching and restore charges, plus normal duration charges, which for SnapStart include init code, runtime load and hook time.

Official documentation

Related on CheatCoders: Lambda cold starts: measure and cut · Lambda response streaming Last updated on October 6, 2026

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.