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
SnapStartNotReadyExceptionwhile Lambda builds a new one. - SnapStart works best at scale. Functions invoked rarely might not see the same improvement.
SnapStart, provisioned concurrency or neither?
| Option | Startup | Cost model | Good fit for agent tools |
|---|---|---|---|
| On-demand (no SnapStart) | Full Init on each new environment | Requests + duration | Light tools, Node.js tools, rarely used tools |
| SnapStart | Restore from snapshot; can be sub-second in good cases | Java: no extra charge. Python/.NET: caching + restore charges | Heavy-init Java, Python or .NET tools with bursty traffic |
| Provisioned concurrency | Pre-initialized; designed for double-digit millisecond response | Extra charge for configured capacity | Tools with steady traffic and strict latency targets |
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
liveinstead of the bare function name.
Step 1: Turn on SnapStart and publish a version
bash
While a version is 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"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: liveAutoPublishAlias 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
Three rules keep this correct:
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}- 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/randomand/dev/urandom. Generators that read from them, such as Python’srandom.SystemRandomor Java’sSecureRandom, stay random. Arandom.seed()call during init does not. - Don’t trust in-memory state for idempotency. Use the
tool_call_idfrom 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
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.
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);
}
}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, theREPORT 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
Run it for a week before enabling SnapStart and a week after, on the same tool and memory size. X-Ray shows a 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 descRestore 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
- The agent’s
parse_symbolstool imports a parsing library and loads grammar files for several languages. Logs show longInit Durationvalues whenever a burst of agent sessions scales the function out. - 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. - You enable SnapStart, publish version 12, wait for
Active, and pointliveat it. Step Functions already invokesagent-parse-tool:live, so nothing else changes. - The Logs Insights query now shows
Restore Durationon cold starts instead ofInit Duration. Compare the cold-start p99 with last week’s to decide whether the caching and restore charges are worth it for this tool. - 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
| Symptom | Likely cause | Fix |
|---|---|---|
| No change in cold starts | Callers invoke $LATEST or the unqualified name | Invoke the alias or a version ARN |
| Invocations fail right after publishing | Version still Pending while the snapshot is created | Wait for State: Active before moving the alias |
SnapStartTimeoutException | Runtime load plus after-restore hooks took longer than 10 seconds | Move work to init or the handler; keep hooks small |
SnapStartNotReadyException on a Java tool | No invocations for 14 days, so the snapshot was deleted | Retry after the version is Active; keep a light schedule if needed |
| Duplicate IDs across environments | IDs or seeds created during init | Generate them in the handler or an after-restore hook; use a CSPRNG |
| Intermittent connection errors after a restore | Non-SDK connections opened during init | Validate and reconnect after restore |
| Can’t enable SnapStart | Unsupported runtime, provisioned concurrency, EFS or ephemeral storage above 512 MB | Remove 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_idin 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
- Improving startup performance with Lambda SnapStart (runtimes, limits, Regions, pricing)
- Activating and managing SnapStart
- Handling uniqueness with SnapStart
- Runtime hooks for Java and for Python
- Maximize SnapStart performance
- Monitoring for SnapStart
- Configuring provisioned concurrency
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- n8n vs AI Agent: I Gave Both the Same Job (Local, Ollama)
- Local AI Video Generator: Build Your Own App (Free, Python, No GPU)
- AI Wrote Raw Binary Instead of Code. Here’s What Happened (WebAssembly)
- I Built a Browser That Only Runs Encrypted Binary (Ep 1)
- RAG From Scratch: Chunking, Embeddings, Reranking and Evaluation (Real Numbers)
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.