Photo by Anne Nygård on Unsplash
To delete old Lambda versions safely, list each function’s published versions with ListVersionsByFunction, list its aliases with ListAliases, and protect $LATEST, every version an alias points to (including weighted routing) and the newest few. Delete the rest one at a time with DeleteFunction and the version number as the Qualifier.
Every deploy that publishes a version leaves an immutable copy of your code behind, and those copies count against the Lambda code storage quota in each region. This example gives you a TypeScript script for the AWS SDK for JavaScript v3 that finds the versions nothing uses, explains why each one stays or goes, and deletes nothing until you pass --apply.
It sits with the other runnable AWS SDK v3 examples for cleanup and cost. If you already call functions by version or alias from code, the example to invoke a Lambda function with AWS SDK v3 in TypeScript shows how a Qualifier works on the calling side.
What does this script do?
- Pick the functionsIt lists every function in the region with
paginateListFunctions, or checks just one when you pass--function my-api. - Find versions that aliases useFor each function,
paginateListAliasesreturns every alias. The script records the alias’sFunctionVersionand every version inRoutingConfig.AdditionalVersionWeights, so a canary or linear deployment that splits traffic between two versions keeps both. - List published versions
paginateListVersionsByFunctionreturns$LATESTplus the numbered versions.$LATESTis dropped immediately; it’s the editable function itself, not an old copy. - Keep the newest NVersions are sorted by number, newest first. The newest three are kept by default so you can roll back quickly; change that with
--keep. - Report, or deleteEvery version is printed with its decision. Only with
--applydoes the script callDeleteFunctionCommandwithQualifierset to the version number. Without a qualifier,DeleteFunctionwould remove the whole function, so the script always sets one.
Prerequisites
- Node.js 18 or later, npm, and
tsxto run TypeScript directly. - The
@aws-sdk/client-lambdapackage. - An AWS profile for the region you want to clean. The dry run only needs the two list statements below.
Which IAM permissions does it need?
lambda:ListFunctions doesn’t support resource-level permissions, so it needs "Resource": "*". The other actions take function ARNs. Lambda matches the policy’s ARN type against the request: an ARN ending in :* after the function name matches qualified requests only (a version or alias), never the unqualified function. That lets the delete statement remove versions without being able to delete a whole function. Replace 123456789012 with your account ID; Lambda doesn’t accept a wildcard there.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListFunctions",
"Effect": "Allow",
"Action": "lambda:ListFunctions",
"Resource": "*"
},
{
"Sid": "ReadVersionsAndAliases",
"Effect": "Allow",
"Action": [
"lambda:ListVersionsByFunction",
"lambda:ListAliases"
],
"Resource": "arn:aws:lambda:*:123456789012:function:*"
},
{
"Sid": "DeleteVersionsOnly",
"Effect": "Allow",
"Action": "lambda:DeleteFunction",
"Resource": "arn:aws:lambda:*:123456789012:function:*:*"
}
]
}
Drop the third statement from the profile you use for dry runs. To draft a policy from your own variant of the script, paste it into the free IAM policy generator for TypeScript code, then review the generated IAM policy for least privilege before you attach it.
The full script to delete old Lambda versions
// delete-old-lambda-versions.ts
// Lists published versions of your Lambda functions and deletes the ones nothing uses.
// Never touches $LATEST, any version an alias points to (including weighted routing),
// or the newest N versions of each function (default 3).
// Report-only by default; pass --apply to delete.
import {
LambdaClient,
paginateListFunctions,
paginateListVersionsByFunction,
paginateListAliases,
DeleteFunctionCommand,
type FunctionConfiguration,
} from "@aws-sdk/client-lambda";
const args = process.argv.slice(2);
const APPLY = args.includes("--apply");
const argValue = (name: string): string | undefined => {
const i = args.indexOf(name);
return i !== -1 ? args[i + 1] : undefined;
};
const KEEP = Number(argValue("--keep") ?? 3);
if (!Number.isInteger(KEEP) || KEEP < 0) throw new Error("--keep must be a whole number (0 or more)");
const ONLY_FUNCTION = argValue("--function"); // optional: one function name or ARN
const region = process.env.AWS_REGION ?? "us-east-1";
const lambda = new LambdaClient({ region });
async function functionNames(): Promise<string[]> {
if (ONLY_FUNCTION) return [ONLY_FUNCTION];
const names: string[] = [];
for await (const page of paginateListFunctions({ client: lambda }, {})) {
for (const fn of page.Functions ?? []) if (fn.FunctionName) names.push(fn.FunctionName);
}
return names;
}
// Every version an alias routes traffic to: the primary version plus weighted extras.
async function versionsUsedByAliases(functionName: string): Promise<Map<string, string[]>> {
const used = new Map<string, string[]>();
const add = (version: string, alias: string) => used.set(version, [...(used.get(version) ?? []), alias]);
for await (const page of paginateListAliases({ client: lambda }, { FunctionName: functionName })) {
for (const alias of page.Aliases ?? []) {
const name = alias.Name ?? "?";
if (alias.FunctionVersion) add(alias.FunctionVersion, name);
for (const extra of Object.keys(alias.RoutingConfig?.AdditionalVersionWeights ?? {})) add(extra, name);
}
}
return used;
}
async function publishedVersions(functionName: string): Promise<FunctionConfiguration[]> {
const versions: FunctionConfiguration[] = [];
for await (const page of paginateListVersionsByFunction({ client: lambda }, { FunctionName: functionName })) {
for (const v of page.Versions ?? []) {
if (v.Version && v.Version !== "$LATEST") versions.push(v); // $LATEST is never a candidate
}
}
// Version numbers only grow, so a numeric sort puts the newest first.
return versions.sort((a, b) => Number(b.Version) - Number(a.Version));
}
async function main(): Promise<void> {
console.log(`${APPLY ? "APPLY" : "DRY RUN"} in ${region}: keep the newest ${KEEP} version(s) per function`);
let planned = 0;
let deleted = 0;
let bytes = 0;
for (const name of await functionNames()) {
const aliasUse = await versionsUsedByAliases(name);
const versions = await publishedVersions(name);
if (versions.length === 0) continue;
console.log(`\n${name}: ${versions.length} published version(s)`);
const toDelete: FunctionConfiguration[] = [];
versions.forEach((v, index) => {
const version = v.Version as string;
let reason = "";
if (aliasUse.has(version)) reason = `alias ${aliasUse.get(version)!.join(", ")}`;
else if (index < KEEP) reason = `one of the newest ${KEEP}`;
console.log(` v${version} ${v.LastModified ?? ""} ${reason ? `keep (${reason})` : "delete"}`);
if (!reason) toDelete.push(v);
});
for (const v of toDelete) {
planned++;
bytes += v.CodeSize ?? 0;
if (!APPLY) continue;
try {
await lambda.send(new DeleteFunctionCommand({ FunctionName: name, Qualifier: v.Version }));
deleted++;
console.log(` deleted ${name}:${v.Version}`);
} catch (err) {
console.error(` could not delete ${name}:${v.Version}: ${(err as Error).message}`);
}
}
}
const mb = (bytes / 1024 / 1024).toFixed(1);
console.log(`\nPlan: delete ${planned} version(s), about ${mb} MB of deployment packages.`);
if (APPLY) console.log(`Deleted ${deleted} of ${planned}.`);
else console.log("Dry run: nothing was changed. Re-run with --apply to delete.");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Every command and paginator used here is documented in the @aws-sdk/client-lambda package in the AWS SDK for JavaScript v3 repository. The size figure adds up CodeSize, the deployment package size of each version, so treat it as an estimate of what you free rather than an exact quota reading.
How do you run it?
npm install @aws-sdk/client-lambda
npm install --save-dev tsx typescript
# Dry run for every function in the region, keeping the newest 3 versions
AWS_PROFILE=readonly AWS_REGION=eu-west-1 npx tsx delete-old-lambda-versions.ts
# One function, keep the newest 5
AWS_PROFILE=readonly AWS_REGION=eu-west-1 npx tsx delete-old-lambda-versions.ts --function orders-api --keep 5
# Delete after reviewing the plan
AWS_PROFILE=admin AWS_REGION=eu-west-1 npx tsx delete-old-lambda-versions.ts --function orders-api --keep 5 --apply
Deleted versions can’t be recovered. Read every “delete” line of the dry run first. Anything outside Lambda that calls a version ARN directly, such as an API Gateway integration, a Step Functions state or an S3 event notification, will start failing once that version is gone. The script can only see aliases.
Sample output
DRY RUN in eu-west-1: keep the newest 3 version(s) per function
orders-api: 9 published version(s)
v42 2026-09-21T10:02:11.000+0000 keep (alias live)
v41 2026-09-18T15:40:03.000+0000 keep (alias live, canary)
v40 2026-09-12T09:12:55.000+0000 keep (one of the newest 3)
v39 2026-09-05T08:30:19.000+0000 delete
v38 2026-08-29T17:01:47.000+0000 delete
v35 2026-08-02T11:22:08.000+0000 keep (alias rollback)
v31 2026-07-14T14:10:36.000+0000 delete
v30 2026-07-10T09:03:29.000+0000 delete
v12 2026-02-01T12:00:00.000+0000 delete
image-resizer: 2 published version(s)
v4 2026-06-30T07:45:10.000+0000 keep (alias prod)
v3 2026-06-01T07:40:02.000+0000 keep (one of the newest 3)
Plan: delete 5 version(s), about 61.4 MB of deployment packages.
Dry run: nothing was changed. Re-run with --apply to delete.
Function names, versions and sizes are illustrative. Version numbers have gaps because deleted versions are never reused, and v35 survives despite its age because an alias still points at it. Provisioned concurrency is configured on a version or alias too, and it bills whether or not requests arrive; the script to find unused Lambda provisioned concurrency checks those configs.
Why delete old Lambda versions at all?
Each published version keeps its own copy of the code. As of September 2026, AWS documents the Lambda-managed storage quota for .zip functions and layers as 300 GB (unzipped) per region, it can’t be increased, and every function version and layer version counts toward it. Teams that publish on every deploy can reach it with a handful of busy functions, and the error arrives at the worst time: during a deploy. The guide to monitor AWS service quota usage and set alerts shows how to watch limits like this before they block you. Functions packaged as container images don’t count toward it; their images live in Amazon ECR, where you can set an ECR lifecycle policy to delete old images. Layer versions pile up the same way when a pipeline publishes one per deploy; the script to find unused Lambda layer versions and delete old ones removes the ones no function version references.
Old versions also make incident work slower. When a function starts failing, a short version list tells you what changed. The walkthrough to investigate Lambda errors with CloudWatch metrics and logs covers that side, and the example that counts Lambda invocations for the last 24 hours helps you spot functions nobody calls at all. For the functions you keep, find Lambda functions on deprecated runtimes before AWS blocks updates to them. Those you redeploy anyway are a good time to switch architecture, and the script to find Lambda functions not running on Graviton arm64 estimates what each would save.
Troubleshooting: why won’t a version delete?
ResourceConflictException. Something changed between the dry run and the delete, for example a deploy moved an alias onto that version. Lambda won’t delete a version that an alias references. Re-run the dry run.AccessDeniedExceptiononDeleteFunction. The policy probably grants the unqualified ARN (function:name), which never matches a request with a qualifier. Use thefunction:*:*pattern above, and check SCPs and permission boundaries with the guide to troubleshoot AWS IAM access denied errors step by step.- Lambda@Edge versions fail to delete. A version replicated to CloudFront edge locations can’t be deleted while a distribution still uses it. Remove the association from the distribution, wait for the replicas to be cleaned up, then try again.
- An event source mapping uses a version ARN. SQS, Kinesis and DynamoDB stream mappings can point at a specific version. The script doesn’t check them; list mappings for the function with
ListEventSourceMappingsbefore you delete, or point mappings at an alias instead. - Provisioned concurrency is on a version. Provisioned concurrency is usually configured on an alias, which the script already protects. If you configured it directly on a version, remove that configuration or raise
--keep.
Ask ChatWithCloud instead
If you just want to know where versions pile up, ask: “Which of my Lambda functions have more than 10 published versions, and which versions do their aliases point to?” ChatWithCloud writes AWS SDK for JavaScript v2 code, runs it on your machine with your AWS profile, and answers from the JSON it gets back; how ChatWithCloud turns questions into AWS SDK calls walks through that loop. Generated code runs without a confirmation step, so use it to explore with a read-only profile and keep deletes for the script above. The guide to connect ChatWithCloud to a read-only AWS profile covers the setup, ChatWithCloud’s security and data handling page lists what leaves your machine, and asking AI about Lambda errors in your account shows the kind of follow-up questions that work well.
Frequently asked questions
Can I delete the $LATEST version of a Lambda function?
No. $LATEST is the function’s editable code and configuration, not a published copy. Removing it means deleting the whole function, which is why the script never considers it.
Does deleting a Lambda version affect its aliases?
Lambda won’t let you delete a version an alias references, and the script skips those versions anyway, including extra versions in a weighted routing configuration.
How many Lambda versions should I keep?
Keep what your rollback process needs. Three recent versions plus whatever your aliases point to covers most teams; raise --keep if you roll back further than that.
Do old Lambda versions cost money?
Versions that aren’t invoked don’t add compute charges, but they do use the region’s code storage quota. The cost is running out of room to deploy, not a line on the bill.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud
