Executive summary
AWS Lambda SnapStart now works for functions packaged as container images, not only as ZIP archives. Same runtimes as before — Java 11 and later, Python 3.12 and later, and .NET 8 and later — and container images can be packaged up to 10 GB.
This is a larger change than it sounds, because of who uses container images. The 10 GB limit is why teams choose that packaging: a model to load, a heavy framework, a pile of native dependencies. Those are precisely the functions with the worst initialisation latency, and until now they were the ones SnapStart could not help.
The mechanism is unchanged and worth stating plainly, because every consideration below follows from it: Lambda initialises your function once when you publish a version, takes a Firecracker MicroVM snapshot of the memory and disk state, encrypts it, caches it — and then resumes every execution environment from that one snapshot. Initialisation does not run again. Anything it produced is not regenerated; it is copied.
What changed
SnapStart previously applied to ZIP-packaged functions. It now supports Java 11 and later, Python 3.12 and later, and .NET 8 and later across ZIP and container image deployment format, with container images up to 10 GB.
What has not changed is the exclusion list, and it is worth reading before planning a migration. Other managed runtimes such as nodejs24.x and ruby4.0, and OS-only runtimes, are not supported. Nor is this available for any function you have not versioned: you can use SnapStart only on published function versions and aliases that point to versions, never on $LATEST.
Availability is all commercial Regions except Asia Pacific (New Zealand) and Asia Pacific (Taipei).
Architecture
A snapshot is shared state
The documentation is unusually direct about the consequence, and it is the thing to internalise before enabling this on anything: if your initialization code generates unique content that is included in the snapshot, then the content might not be unique when it is reused across execution environments. This includes unique IDs, unique secrets, and entropy that's used to generate pseudorandomness.
Read the last clause carefully. It is not only that a UUID generated at init will be the same everywhere — that is obvious once stated. It is that the entropy pool is part of the snapshot, so a random number generator seeded during initialisation produces the same sequence in every environment resumed from it. Code that looks like it generates fresh randomness at request time may not be, if the seeding happened before the snapshot was taken.
Two related cases follow the same shape. The state of connections that your function establishes during the initialization phase isn't guaranteed when Lambda resumes your function from a snapshot — connection pools built at init are pools of handles to sockets that were opened minutes or days ago, from a different machine. And ephemeral data fetched at init, such as temporary credentials or a cached timestamp, is frozen at snapshot time and thawed repeatedly.
Why container images sharpen this
Nothing about the uniqueness problem is new; it has applied to SnapStart since launch. What is new is the population of functions now eligible.
Teams choose container packaging when initialisation is heavy. A ZIP-packaged Python function that imports a couple of libraries has a shallow init and not much opportunity to create something that needed to be unique. A 10 GB image that loads a model, warms a framework, builds a connection pool and fetches a signing key has a deep one. The functions gaining SnapStart today are, on average, the ones with the most initialisation to get wrong.
The exclusions are pointed at the same population
SnapStart does not support provisioned concurrency, Amazon Elastic File System, Amazon S3 Files, or ephemeral storage greater than 512 MB. That last one deserves attention alongside the headline: a 10 GB container image is permitted, but /tmp above 512 MB is not. A function that ships a large image and writes large intermediate files — which is a common shape for media and ML work — can have one or the other.
The EFS exclusion lands in the same place, since a shared filesystem is the other way heavy functions get at large data. Between them, these two limits define which container-packaged functions can actually take the offer.
Business value
Where it fits, the gain is the one AWS advertises: initialisation latency measured in seconds falls to as low as sub-second, without provisioning anything. For a latency-sensitive API whose p99 is dominated by cold starts on a heavy container function, that is a direct improvement to the number users experience, at no engineering cost beyond validation.
The comparison worth making internally is against provisioned concurrency, and the documentation draws it: provisioned concurrency keeps functions initialized and ready to respond in double-digit milliseconds. Use provisioned concurrency if your application has strict cold start latency requirements that can't be adequately addressed by SnapStart. They are also mutually exclusive, so this is a choice rather than a stack: predictable double-digit milliseconds with capacity you pay to keep warm, or sub-second with no reservation.
Security considerations
The security-relevant half of the uniqueness warning is the phrase unique secrets, and it is easy to skim past.
- A secret fetched at init is fetched once, ever. The common pattern of pulling a credential from Secrets Manager outside the handler — good practice specifically to avoid doing it per invocation — means that value is baked into the snapshot and reused by every environment until the version is republished. Rotation no longer takes effect on the next cold start, because there are no more cold starts.
- Anything derived from init-time entropy is shared. Session tokens, nonces, request IDs and idempotency keys generated from a generator seeded before the snapshot repeat across environments. In a signing or deduplication path that is a correctness failure with security consequences, not just a curiosity.
- The snapshot itself is encrypted and Lambda patches it, which addresses the obvious worry about a memory image sitting in a cache. The residual risk is not the storage, it is what your own code put in memory before the picture was taken.
The remedy in all three cases is the same and is structural: move generation of anything that must be unique, current or secret out of initialisation and into the handler, or into a runtime hook that runs after restore.
Cost considerations
The cost model is not uniform across runtimes, and this is the detail most likely to produce a surprise on a Python or .NET estate.
For Java managed runtimes, there's no additional cost for SnapStart. That is the case most people have read about, and it does not generalise. Otherwise there are two charges:
- Caching — for every function version that you publish with SnapStart enabled, you pay for the cost of caching and maintaining the snapshot, priced by configured memory, and you're charged for a minimum of 3 hours. You keep paying as long as the version remains active.
- Restoration — each time a function instance is restored from a snapshot, you pay a restoration charge, again by configured memory.
Two consequences worth planning for. A CI pipeline that publishes a version per commit creates a cached snapshot per commit, each billed for at least three hours, and they keep accruing until someone deletes the versions — AWS links a version-cleanup pattern from the pricing section, which is a fair indication of how often this bites. And because these functions are memory-heavy by nature, both charges scale with the memory setting that container-packaged functions tend to have set high.
One more line that is easy to miss: charges apply each time that Lambda re-runs your initialization code to apply software updates. Snapshot maintenance is not free of your own init cost.
Operational considerations
SnapStart requires published versions and aliases, which is a workflow change for teams deploying to $LATEST. That is a healthier deployment model anyway, but it is a change to pipelines, alias routing and rollback procedure rather than a checkbox.
The testing implication is the one to plan for. A bug caused by snapshot reuse does not appear on the first invocation — it appears on the second environment, which means it does not appear at all under light load. Duplicate identifiers, a stale credential or a repeating random sequence surface when concurrency scales up, in production, and look like intermittent data corruption rather than a startup problem.
So the validation is a load test that forces multiple environments, comparing values across them, not a functional test that confirms the function still returns 200. Failing to do that is how this ships quietly.
Tradeoffs
| Option | Startup | Cost shape | Use when |
|---|---|---|---|
| SnapStart | As low as sub-second | Free on Java managed runtimes; caching plus restore charges otherwise | Heavy init, invocations at scale, and init you can audit for uniqueness |
| Provisioned concurrency | Double-digit milliseconds | You pay to keep environments warm whether used or not | Strict cold-start requirements SnapStart cannot meet. Mutually exclusive with SnapStart |
| Neither | Seconds on a heavy container image | Nothing extra | Batch and asynchronous work where startup latency is not user-visible |
| Slim the image instead | Improves the underlying problem | Engineering time | Always worth doing first — SnapStart hides init cost rather than removing it, and you still pay duration for it |
Implementation guidance
Audit before enabling. The question is not whether the function works, but what its initialisation produced.
# Anything in module scope that is one of these is a finding:
# uuid4(), secrets.token_*(), random.seed(), os.urandom() at import
# get_secret_value() / assume_role() results cached at import
# datetime.now() captured at import
# database or HTTP connection pools built at import
# Move to the handler, or to a restore hook that runs after resume.
def handler(event, context):
request_id = uuid.uuid4() # per invocation, not per snapshot
creds = get_cached_credentials() # refreshed, not frozen
...
Then enable it on a version, and test for the failure that only appears at concurrency.
aws lambda update-function-configuration \
--function-name my-heavy-fn \
--snap-start ApplyOn=PublishedVersions
aws lambda publish-version --function-name my-heavy-fn
# The test that matters: drive enough concurrency to force several
# environments, then assert the values that must differ actually do.
# - collect a per-invocation identifier from each response
# - assert the count of distinct values equals the invocation count
# A functional test on one environment will pass regardless.
Best practices
- Treat module-scope code as "runs once for the lifetime of the version". That is now literally true, and it is a different mental model from "runs once per cold start".
- Fetch secrets in the handler, or refresh them in a restore hook. A credential cached at init outlives its rotation.
- Re-seed anything that generates randomness after restore. The entropy is in the snapshot.
- Delete old published versions. Each one holds a cached snapshot that bills for at least three hours and continues while the version is active.
- Slim the image anyway. Duration charges still apply to initialisation code, and to every re-run Lambda performs to apply patches.
Who should adopt this
Teams running container-packaged Java, Python 3.12+ or .NET 8+ functions behind a latency-sensitive API should evaluate it this week. That is the population this launch was built for, and the improvement on a heavy init is the difference between seconds and sub-second.
Hold off if the function needs more than 512 MB of ephemeral storage, mounts EFS, or currently uses provisioned concurrency — those are exclusions, not frictions. And if the function is invoked infrequently, note AWS's own caveat that SnapStart works best at scale; a rarely used function pays caching charges for a benefit it seldom collects.
Node.js and Ruby estates are unaffected: those runtimes remain unsupported.
Key takeaways
- SnapStart now covers container images up to 10 GB, for Java 11+, Python 3.12+ and .NET 8+ — the packaging choice used precisely by the functions with the heaviest initialisation.
- One snapshot becomes every environment, so unique IDs, secrets and the entropy behind pseudorandomness are duplicated rather than regenerated.
- The exclusions target the same functions: no provisioned concurrency, no EFS, no S3 Files, and no ephemeral storage above 512 MB despite the 10 GB image allowance.
- Cost is free only on Java managed runtimes. Elsewhere, every published version's snapshot is cached with a three-hour minimum, plus a charge per restore.
- The failure mode appears at concurrency, not on the first call — so the validation is a load test that compares values across environments.
Official AWS references
- AWS Documentation — Improving startup performance with Lambda SnapStart
- AWS What's New — Lambda SnapStart for container image functions
- AWS Documentation — Handling uniqueness with Lambda SnapStart
- AWS Documentation — Implement code before or after Lambda function snapshots
- AWS Documentation — Maximize Lambda SnapStart performance
Comments