Learn / AWS Lambda for backend devs / Packaging and dependencies

Lesson 2 of 5 7 min

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 --platform targeting 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.

1. What's the deployment package size that pushes you from a zip deployment toward a container image?
2. Why does installing Python dependencies on your local macOS laptop sometimes break a Lambda function that uses native/compiled packages (e.g. numpy, pillow)?
3. What's a good reason to use a Lambda layer instead of bundling a dependency directly into every function's own package?