Skip to content

Lambda layers: sharing dependencies without version chaos

Lambda layers let functions share dependencies, but unpinned layer versions quietly desynchronize environments. How layers actually work, the limits that bite, and the versioning discipline that prevents chaos.

A Lambda layer is a zip archive that gets extracted into /opt in the execution environment, layered under your function code — a way to ship shared dependencies, custom runtimes, or extensions once instead of bundling them into every function. Layers solve a real packaging problem, but they introduce a versioning problem in exchange: layer versions are immutable and referenced by number, functions never update automatically, and a team without pinning discipline ends up with twelve functions on nine different versions of “the same” layer. The mechanics take ten minutes to learn; the discipline is the actual work.

What’s happening

When you publish a layer, AWS stores it as a new immutable version — version 1, 2, 3, and so on — each with its own ARN ending in the version number. A function references specific layer version ARNs (up to five layers per function), and at cold start Lambda downloads and extracts them into /opt in order, with later layers overwriting earlier ones on path collisions. Function code plus all layers must fit inside the same 250 MB unzipped limit that applies to zip-based functions; layers do not extend it.

For the runtime to find layer content, the zip must follow the runtime’s directory convention: python/ for Python packages, nodejs/node_modules/ for Node.js, java/lib/ for Java, and bin/ for executables that should land on the PATH. Get the folder structure wrong and the layer attaches fine but imports fail — the single most common layer support ticket.

The version chaos arrives through two doors. First, “latest” does not exist: every attachment is to a numbered version, so publishing layer version 14 changes nothing anywhere until each function is updated to reference it — teams that update some functions and forget others drift immediately. Second, deleting a layer version does not break running functions (they keep their copy), but it breaks the next deploy of any function still referencing it, which is how a cleanup script takes down a Friday release.

Two things layers explicitly do not do: they do not reduce cold start time (the same bytes load either way), and they do not act as a package manager — there is no dependency resolution, just zip extraction in order.

What to check first

  • Which layer versions each function references, versus what is current. The spread between them is your drift measurement.
  • Combined size against the 250 MB unzipped limit. Layers plus code share the budget; a fat ML dependency layer can leave no room for the function.
  • Directory structure inside the layer zip. python/lib/python3.12/site-packages/... also works for Python, but a bare site-packages at the root does not.
  • Runtime compatibility. A layer built with native extensions for x86_64 will not load on an arm64 function, and packages compiled on a Mac frequently fail on Amazon Linux.

How to verify

List a layer’s versions and see what a function actually uses:

aws lambda list-layer-versions --layer-name shared-deps \
  --query 'LayerVersions[].[Version,CreatedDate,CompatibleRuntimes[0]]' --output table

aws lambda get-function-configuration --function-name my-function \
  --query 'Layers[].Arn'

Audit drift across the fleet:

aws lambda list-functions \
  --query 'Functions[?Layers != `null`].[FunctionName, Layers[].Arn]' --output json

To see exactly what a layer contains, pull it and inspect the tree:

aws lambda get-layer-version --layer-name shared-deps --version-number 14 \
  --query 'Content.Location' --output text
# curl the presigned URL, unzip, and check the top-level folders

In the console: Lambda > Layers lists layers and versions; a function’s attachments are at Lambda > Functions > (function) > Code, scrolled to the Layers panel. Packaging conventions per runtime are documented at https://docs.aws.amazon.com/lambda/.

The approach

  1. Decide what belongs in a layer at all. Good candidates: a stable shared internal library, a heavy dependency set used by many functions (database drivers, observability agents), vendor extensions, and binaries like ffmpeg. Bad candidates: anything that changes with application code — that belongs in the function package, versioned with the code that uses it.
  2. Build layers in CI, on the target architecture. Build the zip in a container matching the Lambda runtime (Amazon Linux based, correct Python/Node version, correct architecture) so native extensions actually load. Publishing is one call: aws lambda publish-layer-version --layer-name shared-deps --zip-file fileb://layer.zip --compatible-runtimes python3.13 --compatible-architectures arm64.
  3. Pin versions in IaC, always. The layer version ARN lives in Terraform/CloudFormation/SAM alongside the function, so every environment states exactly what it runs and drift is visible in a diff instead of discovered in an incident.
  4. Roll new versions like deploys, not like edits. Publish the new version, update staging references, test, then promote the same version number through environments. The immutability that causes the chaos is also what makes rollback trivial — point back at the old ARN.
  5. Retire old versions deliberately. Only delete a layer version after confirming no function references it (the fleet audit above), and keep the last known-good version until the next one has survived production for a while.

When this is the wrong move

If your dependencies are large or numerous enough that you are fighting the 250 MB limit, stop fighting — container image packaging raises the ceiling to 10 GB and brings normal Docker dependency tooling with it. If only one function uses a dependency, a layer adds indirection with no reuse payoff; bundle it. And do not adopt layers hoping for faster cold starts — that benefit is folklore, as covered in Lambda cold starts: what actually helps.

Common mistakes

  • Wrong directory structure in the zip, producing Unable to import module errors on a layer that attached without complaint.
  • Building on macOS and shipping native .so files that Amazon Linux cannot load.
  • Treating the newest layer version as implicitly deployed everywhere, because nothing in the console warns about the drift.
  • Deleting old layer versions during cleanup and breaking the next deploy of a function nobody remembered still pinned to version 3.

Keeping it under control

  • Run the fleet drift audit monthly and treat more than two active versions of the same layer as a finding.
  • Keep a changelog per layer — version, contents, breaking changes — since AWS stores only a description field.
  • Name layers by content and major version (pg-driver-v2) so incompatible reworks get a new name instead of a surprising version bump.
  • Add a CI check that fails when combined function-plus-layers unzipped size passes an internal threshold below the hard 250 MB.

Layers are a decent packaging tool wearing the costume of a dependency manager — treat every layer version like a release artifact, pinned and promoted through environments, and the chaos never starts. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.