Learn / AWS Lambda for backend devs / Packaging and dependencies
Packaging and dependencies
Zip deployments vs container images, Lambda layers, and keeping a deployment package small enough to actually ship.
Two ways to ship code: zip vs container image
Zip deployment: upload your code and dependencies as a .zip. Simple, fast to deploy, but
capped at 250 MB unzipped including any layers (lesson 1’s quota table). Good default for most
functions - a typical FastAPI-style handler with a handful of pure-Python dependencies fits
comfortably.
Container image: package the function as a Docker image (up to 10 GB), using an AWS base image or your own that implements the Lambda Runtime API. Better fit when:
- dependencies are heavy (ML frameworks, native libraries, large model files),
- you already have a Docker-based build pipeline and want one artifact format across services,
- you need more control over the underlying OS packages than a zip + layers setup gives you.
Neither is universally “better” - zip deploys faster and is simpler for small functions; container images scale to dependencies zip can’t hold.
Layers: shared dependencies, smaller per-function packages
A layer is a separate zip of libraries/code that one or more functions reference at
runtime, extracted into /opt inside the execution environment. The practical benefit: if
five functions all use the same internal utility library or the same third-party SDK, that
dependency lives in one layer instead of being duplicated five times, and each function’s own
deployment package - the part that actually changes on most of your deploys - stays small and
fast to upload.
my-function.zip <- your handler + code that changes often
|
+ layer: shared-deps.zip <- boto3 extras, internal utils - changes rarely
Keep layers for genuinely shared, slow-changing dependencies. A dependency used by only one function doesn’t need a layer - just bundle it directly.
Build for the target, not your laptop
Any dependency with compiled/native code (numpy, pillow, cryptography’s native backend, many
ML libraries) is built for a specific OS and CPU architecture. Installing it locally on macOS
or Windows and zipping that up will fail at runtime on Lambda’s Linux execution environment -
often with an opaque ImportError deep in a native extension, not an obvious “wrong platform”
message.
Practical approaches:
- Build inside a container matching the Lambda runtime (AWS publishes Amazon Linux-based base images for exactly this).
- Use
pip install --platformtargeting the correct manylinux tag and Lambda’s CPU architecture (x86_64 or arm64 - pick one and be consistent, since a mismatch here is the same class of bug). - Prefer arm64 (Graviton) for new functions where your dependencies support it - AWS’s own guidance is that it’s typically both cheaper and faster for compute-bound workloads, but confirm your dependencies actually ship arm64-compatible builds before committing to it.
Smaller packages, faster cold starts
Every unused dependency in your package adds to what has to be downloaded and initialized during a cold start (lesson 3). Trim what you don’t use - a stray heavy library imported for one rarely-used code path, brought in “just in case,” is a tax paid on every cold start, not just when that path runs.
Key takeaways
- A zip deployment is capped at 250 MB unzipped (including layers) - a container image (up to 10 GB) is the right choice once real dependencies (ML libraries, native binaries) push past that.
- Layers let you share common dependencies across multiple functions and keep your own deployment package's diff small and fast to update.
- Install dependencies for the Lambda runtime's target platform, not your local machine - a package with native (compiled) code built on macOS/Windows will not run on Lambda's Linux/x86 or Linux/arm64 environment.
- Smaller packages cold-start faster - trimming unused dependencies isn't just tidiness, it's a real latency lever (lesson 3).
Quick check
3 questions - see how much stuck.