Find Unused Lambda Layer Versions and Delete Old Ones

Lines of source code on a dark monitor screen in a dim room

Photo by Markus Spiske on Unsplash

To delete old Lambda layer versions safely, first find the ones nothing uses: list every layer version with ListLayers and ListLayerVersions, then collect the layer ARNs that functions reference with ListFunctions and FunctionVersion=ALL, which includes published versions. Versions no function uses, and that aren’t shared with other accounts, are candidates for DeleteLayerVersion. Deletion is permanent, and version numbers are never reused.

Every publish-layer-version in a deploy pipeline adds an immutable layer version, and nothing removes the old ones. Months later a layer can have dozens of versions, most of them pinned by nobody, each carrying an old copy of your dependencies.

This example is for engineers who own shared Lambda layers. The script lists every layer version in a Region, shows which function versions use it, looks up the size and sharing policy of the unused ones, and reports the account’s code storage use. With --apply it deletes old Lambda layer versions in the layers you name, always keeping the newest ones and skipping versions shared with other accounts unless you say otherwise.

Why clean up old layer versions?

Storage quota. Lambda’s quotas page lists 300 GB (unzipped) per Region for .zip functions and layers in Lambda-managed storage, and says every function version and layer version counts toward it. The quota can’t be raised; the alternative AWS points to is self-managed S3 code storage. GetAccountSettings returns your current usage and limit, and the script prints both.

Old dependencies. A layer version is a frozen set of libraries. Keeping unused ones around makes it easy for someone to attach a stale version to a new function. The OWASP Top 10 2025 entry on software supply chain failures lists not tracking the versions of the components you use, and running out-of-date ones, among the signs you’re exposed. Fewer versions means fewer to track.

Function versions have the same problem. If both are growing, start with the example to delete old and unused Lambda function versions: removing old function versions is often what makes a layer version unused in the first place.

What happens when you delete a layer version?

  • The version can no longer be viewed or added to functions, and its number is never reused for that layer name.
  • Functions that already reference it keep running with the layer content. Lambda keeps a copy until no function refers to it, and deleting a layer version never makes a function inactive.
  • For a layer in Lambda-managed storage, AWS says the deleted version stops counting toward the account’s storage quota, even while functions still reference it. The content then counts only toward each function’s 250 MB unzipped deployment package size.
  • Layers stored in your own S3 bucket don’t use Lambda-managed storage, so deleting them frees no quota.

Sharing is the risk you can’t see from your own account. A layer version can grant lambda:GetLayerVersion to another account, every account, or an organization, and a function in another account may depend on it. The script calls GetLayerVersionPolicy and treats any policy as “shared”.

What does the script do?

  1. Maps layer usagepaginateListFunctions with FunctionVersion: "ALL" returns $LATEST and every published version, each with its Layers ARNs.
  2. Lists layer versionspaginateListLayers, then paginateListLayerVersions per layer, newest first.
  3. Keeps the newestThe newest --keep versions of each layer (default 1) are never candidates, used or not.
  4. Inspects candidatesGetLayerVersion gives Content.CodeSize; GetLayerVersionPolicy shows whether it’s shared.
  5. Reports storageGetAccountSettings returns AccountUsage.TotalCodeSize and AccountLimit.TotalCodeSize.
  6. Deletes, if askedWith --apply --layers, DeleteLayerVersion runs for unshared candidates in those layers only.

Prerequisites

  • Node.js 18 or later with tsx, plus @aws-sdk/client-lambda. The paginate* helpers are explained in the guide to AWS SDK v3 paginators.
  • Run it in every account that uses your layers if you share them, and in each Region you publish to.
  • Check deploy tooling first. A CloudFormation, SAM or CDK template can pin an exact layer version ARN, and a deleted version can’t be added to a function again, so update templates before you delete what they reference.

Which IAM permissions does it need?

lambda-layer-cleanup-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListFunctionsAndLayers",
      "Effect": "Allow",
      "Action": ["lambda:ListFunctions", "lambda:ListLayers", "lambda:ListLayerVersions", "lambda:GetAccountSettings"],
      "Resource": "*"
    },
    {
      "Sid": "InspectLayerVersions",
      "Effect": "Allow",
      "Action": ["lambda:GetLayerVersion", "lambda:GetLayerVersionPolicy"],
      "Resource": "arn:aws:lambda:*:111122223333:layer:*:*"
    },
    {
      "Sid": "DeleteOnlyWithApply",
      "Effect": "Allow",
      "Action": "lambda:DeleteLayerVersion",
      "Resource": "arn:aws:lambda:*:111122223333:layer:*:*"
    }
  ]
}

Leave out the last statement for a report-only role, or narrow it to the layer names you manage, such as layer:shared-deps:*. The IAM policy generator for TypeScript code drafts a policy like this from the script, which you can then check against the guide to reviewing IAM policies for least privilege.

The script to delete old Lambda layer versions

find-unused-lambda-layer-versions.ts

// find-unused-lambda-layer-versions.ts
// Lists every Lambda layer version in a Region, finds the ones that no function version in this account
// references ($LATEST and every published version), and shows their size, whether they are shared with other
// accounts, and the account's code storage usage. The newest --keep versions of each layer are always kept.
// --apply deletes unused versions in the layers you name. Deletion can't be undone.
// Usage:
//   npx tsx find-unused-lambda-layer-versions.ts [--region us-east-1] [--keep 2]
//   npx tsx find-unused-lambda-layer-versions.ts --region us-east-1 --keep 2 --apply --layers shared-deps,pandas [--include-shared]
import {
  LambdaClient,
  DeleteLayerVersionCommand,
  GetAccountSettingsCommand,
  GetLayerVersionCommand,
  GetLayerVersionPolicyCommand,
  ResourceNotFoundException,
  paginateListFunctions,
  paginateListLayers,
  paginateListLayerVersions,
} from "@aws-sdk/client-lambda";

const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
  const i = args.indexOf(name);
  return i >= 0 ? args[i + 1] : undefined;
};
const region = flag("--region") ?? process.env.AWS_REGION ?? "us-east-1";
const keep = Number(flag("--keep") ?? "1");
const apply = args.includes("--apply");
const includeShared = args.includes("--include-shared");
const applyLayers = new Set((flag("--layers") ?? "").split(",").map((s) => s.trim()).filter(Boolean));
if (!Number.isInteger(keep) || keep < 0) {
  console.error("--keep must be a whole number (0 or more)");
  process.exit(1);
}

const lambda = new LambdaClient({ region });
const mb = (bytes: number): number => Math.round((bytes / 1024 / 1024) * 10) / 10;
const errText = (err: unknown): string => (err instanceof Error ? `${err.name}: ${err.message}` : String(err));

interface Row {
  Layer: string;
  Version: number;
  Created: string;
  SizeMB: string;
  UsedBy: string;
  Shared: string;
  Finding: string;
}

async function isShared(layerName: string, version: number): Promise<boolean> {
  try {
    const out = await lambda.send(new GetLayerVersionPolicyCommand({ LayerName: layerName, VersionNumber: version }));
    return !!out.Policy;
  } catch (err) {
    if (err instanceof ResourceNotFoundException) return false; // no resource-based policy
    throw err;
  }
}

async function main(): Promise<void> {
  // Which layer versions do function versions in this account and Region use?
  const usedBy = new Map<string, string[]>();
  for await (const page of paginateListFunctions({ client: lambda }, { FunctionVersion: "ALL" })) {
    for (const fn of page.Functions ?? []) {
      for (const layer of fn.Layers ?? []) {
        if (!layer.Arn) continue;
        usedBy.set(layer.Arn, [...(usedBy.get(layer.Arn) ?? []), `${fn.FunctionName}:${fn.Version}`]);
      }
    }
  }

  const rows: Row[] = [];
  const candidates: { layer: string; version: number; sizeBytes: number; shared: boolean }[] = [];
  for await (const page of paginateListLayers({ client: lambda }, {})) {
    for (const layer of page.Layers ?? []) {
      if (!layer.LayerName) continue;
      const versions: { arn: string; version: number; created: string }[] = [];
      for await (const vPage of paginateListLayerVersions({ client: lambda }, { LayerName: layer.LayerName })) {
        for (const v of vPage.LayerVersions ?? []) {
          if (v.LayerVersionArn && v.Version !== undefined) {
            versions.push({ arn: v.LayerVersionArn, version: v.Version, created: (v.CreatedDate ?? "").slice(0, 10) });
          }
        }
      }
      versions.sort((a, b) => b.version - a.version);
      for (const [index, v] of versions.entries()) {
        const users = usedBy.get(v.arn) ?? [];
        let finding = "in use";
        let sizeBytes = 0;
        let shared = false;
        if (!users.length && index < keep) finding = "unused (kept: newest)";
        if (!users.length && index >= keep) {
          const detail = await lambda.send(new GetLayerVersionCommand({ LayerName: layer.LayerName, VersionNumber: v.version }));
          sizeBytes = detail.Content?.CodeSize ?? 0;
          shared = await isShared(layer.LayerName, v.version);
          finding = shared ? "UNUSED here, but shared with other accounts" : "UNUSED: delete candidate";
          candidates.push({ layer: layer.LayerName, version: v.version, sizeBytes, shared });
        }
        rows.push({
          Layer: layer.LayerName,
          Version: v.version,
          Created: v.created,
          SizeMB: sizeBytes ? String(mb(sizeBytes)) : "-",
          UsedBy: users.length ? `${users.length} (${users.slice(0, 2).join(", ")}${users.length > 2 ? ", ..." : ""})` : "-",
          Shared: shared ? "yes" : users.length || index < keep ? "-" : "no",
          Finding: finding,
        });
      }
    }
  }
  console.table(rows);

  const settings = await lambda.send(new GetAccountSettingsCommand({}));
  const usedStorage = settings.AccountUsage?.TotalCodeSize ?? 0;
  const limit = settings.AccountLimit?.TotalCodeSize ?? 0;
  const reclaim = candidates.filter((c) => !c.shared).reduce((sum, c) => sum + c.sizeBytes, 0);
  console.log(
    `${rows.length} layer versions in ${region}: ${candidates.length} unused beyond the newest ${keep} per layer ` +
      `(${candidates.filter((c) => c.shared).length} shared). Code storage: ${mb(usedStorage)} MB of ${mb(limit)} MB; ` +
      `unshared candidates add up to ${mb(reclaim)} MB of layer archives.`,
  );

  if (!apply) {
    console.log("Report only: nothing was deleted. Use --apply --layers <a,b> to delete unused versions of those layers.");
    return;
  }
  if (!applyLayers.size) {
    console.error("--apply needs --layers");
    process.exit(1);
  }
  for (const c of candidates) {
    if (!applyLayers.has(c.layer)) continue;
    if (c.shared && !includeShared) {
      console.log(`Skipping ${c.layer}:${c.version}: shared with other accounts (add --include-shared to delete it)`);
      continue;
    }
    try {
      await lambda.send(new DeleteLayerVersionCommand({ LayerName: c.layer, VersionNumber: c.version }));
      console.log(`Deleted ${c.layer}:${c.version} (${mb(c.sizeBytes)} MB)`);
    } catch (err) {
      console.error(`Could not delete ${c.layer}:${c.version}: ${errText(err)}`);
      process.exitCode = 1;
    }
  }
}

main().catch((err) => {
  console.error(errText(err));
  process.exit(1);
});

How do you run it?

Terminal

npm install @aws-sdk/client-lambda
npm install --save-dev tsx typescript @types/node

# Report only, keeping the newest 2 versions of each layer
AWS_PROFILE=readonly npx tsx find-unused-lambda-layer-versions.ts --region us-east-1 --keep 2

# Delete unused, unshared versions of two layers
AWS_PROFILE=lambda-admin npx tsx find-unused-lambda-layer-versions.ts --region us-east-1 --keep 2 \
  --apply --layers shared-deps,pandas

Sample output

Output (report only, –keep 1)

┌─────────┬───────────────┬─────────┬──────────────┬────────┬────────────────────────┬────────┬───────────────────────────────────────────────┐
│ (index) │ Layer         │ Version │ Created      │ SizeMB │ UsedBy                 │ Shared │ Finding                                       │
├─────────┼───────────────┼─────────┼──────────────┼────────┼────────────────────────┼────────┼───────────────────────────────────────────────┤
│ 0       │ 'shared-deps' │ 7       │ '2026-07-01' │ '-'    │ '1 (checkout:$LATEST)' │ '-'    │ 'in use'                                      │
│ 1       │ 'shared-deps' │ 6       │ '2026-06-01' │ '-'    │ '1 (checkout:12)'      │ '-'    │ 'in use'                                      │
│ 2       │ 'shared-deps' │ 5       │ '2026-05-01' │ '36'   │ '-'                    │ 'no'   │ 'UNUSED: delete candidate'                    │
│ 3       │ 'shared-deps' │ 4       │ '2026-04-01' │ '29'   │ '-'                    │ 'no'   │ 'UNUSED: delete candidate'                    │
│ 4       │ 'shared-deps' │ 3       │ '2026-03-01' │ '22'   │ '-'                    │ 'no'   │ 'UNUSED: delete candidate'                    │
│ 5       │ 'pandas'      │ 4       │ '2026-04-01' │ '-'    │ '-'                    │ '-'    │ 'unused (kept: newest)'                       │
│ 6       │ 'pandas'      │ 3       │ '2026-03-01' │ '-'    │ '1 (reports:$LATEST)'  │ '-'    │ 'in use'                                      │
│ 7       │ 'pandas'      │ 2       │ '2026-02-01' │ '15'   │ '-'                    │ 'yes'  │ 'UNUSED here, but shared with other accounts' │
│ 8       │ 'pandas'      │ 1       │ '2026-01-01' │ '8'    │ '-'                    │ 'no'   │ 'UNUSED: delete candidate'                    │
└─────────┴───────────────┴─────────┴──────────────┴────────┴────────────────────────┴────────┴───────────────────────────────────────────────┘
9 layer versions in us-east-1: 5 unused beyond the newest 1 per layer (1 shared). Code storage: 9216 MB of 307200 MB; unshared candidates add up to 95 MB of layer archives.
Report only: nothing was deleted. Use --apply --layers <a,b> to delete unused versions of those layers.

Layer names and sizes are illustrative. shared-deps versions 6 and 7 are in use, one by the $LATEST code and one by published version 12 of checkout, which is why including published versions matters. Versions 3 to 5 are unused and unshared: 87 MB of candidates. In pandas, version 4 is unused but kept as the newest, and version 2 is shared with other accounts, so --apply skips it until you’ve asked those accounts. The storage line shows this account is far from its quota, so here the cleanup is about hygiene more than headroom.

Troubleshooting

  • AccessDeniedException on GetLayerVersionPolicy. The role lacks the second statement. The guide to troubleshooting AWS IAM access denied errors shows how to find which policy blocks it.
  • TooManyRequestsException. Many layers mean many API calls. The SDK retries throttled calls; for very large accounts, the guide to AWS SDK v3 retries and timeouts shows how to raise maxAttempts.
  • A deploy fails after cleanup. Check whether a template still references a deleted version. Point it at a current version; you can’t recreate a deleted version number.
  • Code storage barely moves. Most of it may be function versions rather than layers. The SizeMB column shows each layer archive’s size, which isn’t the same measure as the quota’s unzipped figure.

Ask ChatWithCloud instead

To see the picture without a script, ask ChatWithCloud “Which Lambda layer versions in us-east-1 aren’t used by any function version?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your profile and summarizes the result. Keep deletions to the script: ChatWithCloud runs changes without a confirmation step, which is why the ChatWithCloud security page recommends a read-only profile. The guide to asking AI about Lambda errors in your account shows other Lambda questions it handles.

Frequently asked questions

Does deleting a Lambda layer version break functions that use it?

No. Functions that already reference it keep running with the layer content. You just can’t add that version to a function again.

Can I restore a deleted Lambda layer version?

No. Deletion is permanent and Lambda never reuses a version number. Publish the same content again to get a new version.

Do Lambda layers count toward the code storage quota?

Yes, when they use Lambda-managed storage. The quotas page lists 300 GB (unzipped) per Region for .zip functions and layers together, and it can’t be increased.

How many layers can a Lambda function use?

Up to five, and the unzipped size of the function plus all its layers must stay within 250 MB.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud