Skip to content

How Lambda Packaging Format Changes Cold Start Time

10 min read · updated August 11, 2026

The choice between a zip archive and a 10 GB container image is usually forced by dependency size rather than chosen, and it is surrounded by benchmark folklore. What can be stated without running anything are the documented limits, the published loading mechanism, and the billing rule that changed in 2025 — which together explain most of what people observe.

The documented limits

From the AWS Lambda quotas page, at the time of writing:

  • Zip archive: 50 MB zipped when uploaded through the Lambda API, SDKs or console; larger archives must go via Amazon S3. The hard one is 250 MB, the maximum size of the deployment package contents unzipped, including layers and custom runtimes.
  • Layers: 5 per function, counting against that same 250 MB.
  • Container image: 10 GB maximum uncompressed image size including all layers, with the image stored in Amazon ECR under ECR’s own quotas. Container image settings are capped at 16 KB.
  • Shared with both: memory from 128 MB to 10,240 MB in 1 MB increments, with one vCPU equivalent at 1,769 MB; /tmp between 512 MB and 10,240 MB; a 900-second timeout.

That 250 MB ceiling is why this question exists at all for AI workloads. A function that imports a provider SDK and a couple of small helpers fits comfortably. One that pulls in a tokenizer library, a vector client and a data-processing stack does not, and there is no configuration that raises the number.

How Lambda loads a container image

The naive model — Lambda pulls a 4 GB image before your handler runs — is not what happens, and AWS has published the design in detail. “On-demand Container Loading in AWS Lambda” by Brooker, Danilov, Greenwood and Piwonka, USENIX ATC 2023, describes how images up to 10 GiB are supported while adding as many as 15,000 new containers per second.

The load-bearing ideas in the paper are these. The image is flattened into a single filesystem and split into fixed-size chunks. Chunks are deduplicated across all customers, which is effective because the same base images recur constantly — the paper reports that roughly 80% of newly uploaded functions produce zero unique chunks, being re-uploads of images already seen. Deduplication is reconciled with encryption by convergent encryption: the key for a chunk is derived deterministically from a cryptographic hash of its contents, so identical plaintext yields identical ciphertext and can be stored once. Chunks are then served through a tiered cache and loaded on demand, block by block, as the filesystem is read.

Two predictions follow directly, and both match what practitioners report. First, the size of the image matters far less than how much of it is read on the startup path, for the same reason that applies on Cloud Run with image streaming. Second, the very first cold start after deploying a new image is the expensive one, because no cache tier holds its unique chunks yet; subsequent cold starts hit a warmer tier. A benchmark that deploys and immediately measures once is measuring the worst case by construction.

What that does to the INIT phase

Lambda’s execution environment lifecycle has three phases: Init, Invoke and Shutdown. Init is where the runtime starts and your code outside the handler executes, and it is what Init Duration in the REPORT log line reports. The packaging format influences it through what has to be read before your first line runs — a runtime plus your code, in both cases, but assembled differently.

What the format does not change is the part that usually dominates for an AI function: importing the SDK, constructing the client, reading configuration, and fetching a secret. A Python function that imports a large dependency tree pays for those imports identically whether the bytes arrived as a zip or as image chunks. This is worth stating plainly because it is the most common misattribution in this area — a slow init gets blamed on the packaging format when a profile of the import graph would name a single library.

The other structural difference is eligibility rather than speed: SnapStart does not support container images. Choosing a container image to escape the 250 MB limit forfeits the feature designed to remove init latency, which makes it a decision about init cost rather than only about package size.

INIT billing changed in August 2025

Any comparison written before mid-2025 is arithmetically out of date. AWS standardised INIT phase billing effective 1 August 2025: the INIT phase is now billed across all configuration types, and its duration is included in Billed Duration for on-demand invocations of managed-runtime functions with zip packaging as well. AWS’s announcement notes that only that one combination — on-demand invocations of zip-packaged functions on managed runtimes — had previously gone unbilled; functions on custom runtimes, on provisioned concurrency, or packaged as OCI images already included it.

The consequence is that the zip format lost a cost advantage it used to have, and the advantage was in billing rather than in latency. AWS expects minimal impact on most bills because INIT occurs on a small fraction of invocations — but a function with high cold-start exposure and a heavy import graph is precisely the one where the fraction is not small, and that is the same function this page is about.

Every figure on this page is a documented value as of August 2026: quotas from the Lambda quotas page, the loading design from the 2023 USENIX paper, the billing change from the AWS compute blog. Quotas move. Re-read the quotas page rather than trusting these numbers indefinitely.

Measuring it on your own functions

The only comparison that means anything is one run on your dependency tree, in your region, at your memory setting. Deploy the same handler both ways, at the same memory, and read the numbers Lambda already emits. This CloudWatch Logs Insights query separates cold from warm invocations and reports percentiles rather than a mean, because one cold start drags a mean past every invocation anybody experienced:

fields @timestamp, @duration, @initDuration, @billedDuration, @memorySize, @maxMemoryUsed
| filter @type = "REPORT"
| stats
    count(*) as invocations,
    pct(@initDuration, 50) as p50_init,
    pct(@initDuration, 90) as p90_init,
    pct(@initDuration, 99) as p99_init,
    pct(@duration, 50) as p50_duration
  by ispresent(@initDuration) as cold_start

Run it over both functions’ log groups for the same window. Three rules make the result honest. Give the new image several cold starts before you count, so you are not measuring the first-deploy cache miss the paper predicts. Hold memory constant, because CPU is allocated in proportion to it and a different memory setting changes init time independently of packaging. And compare the same handler code — a container image built from a different base with a different Python version is two variables.

If the container variant is slower and you need it, the levers are ordinary: order layers so the volatile ones are last, keep the startup read path small, defer imports that only some code paths need, and move secret fetching out of init and behind a cache. If the numbers come out close, the packaging format was never the question and the import graph was.