Photo by Alexandre Debiève on Unsplash
To plan a Lambda arm64 Graviton migration, list every function with ListFunctions and keep those whose Architectures is x86_64 or missing. For each, check the runtime is still supported, the package type, and each layer’s CompatibleArchitectures. Then sum the Duration metric to estimate the saving: arm64 duration costs 20% less per GB-second in us-east-1.
Every Lambda function runs on one instruction set architecture, and the default is still x86_64. Moving a function to arm64, which runs on AWS Graviton2 processors, lowers its duration price with no change to memory or code logic. The work is in the details: native packages, layers and container images are built for one architecture.
This example is for developers and platform engineers who want a ranked list before they start a Lambda arm64 Graviton migration. The script lists every x86_64 function in the Regions you pass, marks what would block the move, and estimates the monthly saving from real usage. It’s report only and never changes a function.
What do you save by moving a Lambda function to arm64?
As of September 2026, the AWS Price List gives these first-tier us-east-1 duration rates: $0.0000166667 per GB-second on x86_64 and $0.0000133334 on arm64. That’s 20% less for the same GB-seconds. Requests cost $0.20 per million on both. The script to find Lambda functions with too much memory has the full price table and the free tier, so it isn’t repeated here. Provisioned concurrency has its own per-GB-second charge that runs whether requests arrive or not; the script to find unused Lambda provisioned concurrency shows which configs sit idle.
Worked example: orders-api has 1,024 MB and ran 30 million times last month with 3,600,000 seconds of total duration. That’s 3,600,000 × 1 GB = 3,600,000 GB-seconds. On x86_64 it costs 3,600,000 × $0.0000166667 = $60.00; on arm64, 3,600,000 × $0.0000133334 = $48.00. The saving is $12.00 a month, if duration stays the same. It may not: some workloads run faster on Graviton and some slower, which is why the migration steps below include a side-by-side test.
What can block a Lambda arm64 Graviton migration?
According to the Lambda Developer Guide, all supported Lambda runtimes run on both architectures. The blockers are in what you deploy with them:
- Deprecated runtimes. Upgrade the runtime first; the script to find Lambda functions on deprecated runtimes covers that job. This script flags any runtime that isn’t in the current supported list.
- Native dependencies. Pure JavaScript, Python and Java code runs unchanged. Packages that ship compiled binaries, such as image, crypto or database drivers, must be installed for arm64. In Node.js these are C++ addons, which the Node.js documentation on C++ addons describes as dynamically linked shared objects. One built on an x86 laptop won’t load on Graviton.
- Layers. Every layer must have an arm64 build. A layer version can declare
CompatibleArchitectures; the script reads it withGetLayerVersionByArn. A layer that declares nothing may still work, so it’s marked for a manual check. - Container images. An image is built for one architecture, so an image function needs a new arm64 (or multi-architecture) image.
- Extensions and agents. Monitoring extensions are binaries too. Check that each vendor ships an arm64 build.
No API tells you what’s inside a zip file, so native dependencies are the one blocker the script can’t see. The checklist further down covers them.
What does the script do?
- Lists functions
paginateListFunctionsin each Region. A function whoseArchitecturesis missing is treated asx86_64, the default. - Checks the runtime and package typeImage functions need a rebuild. Zip functions on a runtime outside the supported list need an upgrade first.
- Reads each layer
GetLayerVersionByArnreturnsCompatibleArchitectures. Results are cached, because many functions share a layer. - Measures usage
GetMetricDatasumsDurationandInvocationsfromAWS/Lambdaover--days(default 30), 250 functions per request. - Estimates costGB-seconds = total duration in seconds × memory in GB, priced at both rates and scaled to 30 days.
- Ranks the listSorted by monthly saving, with a verdict per function. Nothing is changed.
Prerequisites
- Node.js 18 or later with
tsx, plus@aws-sdk/client-lambdaand@aws-sdk/client-cloudwatch. - A read-only AWS profile the SDK can resolve; the guide to AWS SDK v3 credential providers with fromIni, fromSSO and assume role shows the options.
- The Regions where you run Lambda. Prices in the script are for us-east-1; adjust them elsewhere.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListFunctionsAndMetrics",
"Effect": "Allow",
"Action": ["lambda:ListFunctions", "cloudwatch:GetMetricData"],
"Resource": "*"
},
{
"Sid": "ReadLayerVersions",
"Effect": "Allow",
"Action": "lambda:GetLayerVersion",
"Resource": "arn:aws:lambda:*:*:layer:*:*"
}
]
}
GetLayerVersionByArn is authorized as lambda:GetLayerVersion. For a layer owned by another account, that account’s layer permissions decide, so the script reports it as “unreadable” instead of failing. If you extend the script, the IAM policy generator for TypeScript code lists the new actions.
The script to find Lambda functions not on arm64
// find-lambda-functions-not-on-arm64.ts
// Lists Lambda functions still on x86_64, checks what blocks a move to arm64 (Graviton):
// runtime, package type and layer CompatibleArchitectures, and estimates the duration cost
// you'd save from the CloudWatch Duration metric. Report only: it never changes a function.
// Usage: npx tsx find-lambda-functions-not-on-arm64.ts [--regions us-east-1,eu-west-1] [--days 30] [--csv arm64.csv]
import { writeFileSync } from "node:fs";
import {
GetLayerVersionByArnCommand,
LambdaClient,
paginateListFunctions,
type FunctionConfiguration,
} from "@aws-sdk/client-lambda";
import { CloudWatchClient, paginateGetMetricData, type MetricDataQuery } from "@aws-sdk/client-cloudwatch";
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 regions = (flag("--regions") ?? process.env.AWS_REGION ?? "us-east-1").split(",").map((r) => r.trim()).filter(Boolean);
const days = Number(flag("--days") ?? 30);
const csvPath = flag("--csv");
// us-east-1 on-demand duration price per GB-second, first tier. AWS Price List, September 2026.
const X86_PER_GB_S = 0.0000166667;
const ARM_PER_GB_S = 0.0000133334;
// Supported runtime identifiers (Lambda runtimes page, September 2026). All of them run on both architectures.
const SUPPORTED_RUNTIMES = new Set([
"nodejs24.x", "nodejs22.x", "python3.14", "python3.13", "python3.12", "python3.11", "python3.10",
"java25", "java21", "java17.al2023", "java11.al2023", "java8.al2023", "java17", "java11", "java8.al2",
"dotnet10", "dotnet8", "ruby4.0", "ruby3.4", "ruby3.3", "provided.al2023",
]);
interface Row {
Region: string;
Function: string;
Runtime: string;
Package: string;
MemoryMB: number;
Layers: string;
Invocations: number;
GbSeconds: number;
X86PerMonth: string;
SavePerMonth: string;
Verdict: string;
}
type LayerArch = "arm64 ok" | "x86_64 only" | "not declared" | "unreadable";
const layerCache = new Map<string, LayerArch>();
async function layerArch(lambda: LambdaClient, arn: string): Promise<LayerArch> {
const cached = layerCache.get(arn);
if (cached) return cached;
let result: LayerArch;
try {
const res = await lambda.send(new GetLayerVersionByArnCommand({ Arn: arn }));
const archs = res.CompatibleArchitectures ?? [];
result = archs.length === 0 ? "not declared" : archs.includes("arm64") ? "arm64 ok" : "x86_64 only";
} catch {
result = "unreadable"; // usually a layer in another account whose policy doesn't grant GetLayerVersion
}
layerCache.set(arn, result);
return result;
}
/** Sum of Duration (ms) and Invocations per function over the window, 250 functions per GetMetricData call. */
async function usage(cw: CloudWatchClient, names: string[]): Promise<Map<string, { durationMs: number; invocations: number }>> {
const out = new Map<string, { durationMs: number; invocations: number }>();
const end = new Date();
const start = new Date(end.getTime() - days * 86_400_000);
for (let i = 0; i < names.length; i += 250) {
const chunk = names.slice(i, i + 250);
const queries: MetricDataQuery[] = chunk.flatMap((name, j) => (["Duration", "Invocations"] as const).map((metric) => ({
Id: `${metric === "Duration" ? "d" : "n"}${j}`,
MetricStat: {
Metric: { Namespace: "AWS/Lambda", MetricName: metric, Dimensions: [{ Name: "FunctionName", Value: name }] },
Period: days * 86_400,
Stat: "Sum",
},
})));
for await (const page of paginateGetMetricData({ client: cw }, { StartTime: start, EndTime: end, MetricDataQueries: queries })) {
for (const r of page.MetricDataResults ?? []) {
const name = chunk[Number((r.Id ?? "").slice(1))];
if (name === undefined) continue;
const entry = out.get(name) ?? { durationMs: 0, invocations: 0 };
const sum = (r.Values ?? []).reduce((a, b) => a + b, 0);
if (r.Id?.startsWith("d")) entry.durationMs += sum;
else entry.invocations += sum;
out.set(name, entry);
}
}
}
return out;
}
async function verdictFor(lambda: LambdaClient, fn: FunctionConfiguration): Promise<{ layers: string; verdict: string }> {
const layerArns = (fn.Layers ?? []).map((l) => l.Arn ?? "").filter(Boolean);
const states = await Promise.all(layerArns.map((arn) => layerArch(lambda, arn)));
const layers = layerArns.length === 0 ? "none" : states.map((s, i) => `${layerArns[i]?.split(":")[6] ?? "?"}: ${s}`).join("; ");
if (fn.PackageType === "Image") return { layers, verdict: "rebuild the container image for arm64" };
if (!fn.Runtime || !SUPPORTED_RUNTIMES.has(fn.Runtime)) return { layers, verdict: "upgrade the runtime first" };
if (states.includes("x86_64 only")) return { layers, verdict: "blocked: replace x86_64-only layer" };
if (states.includes("not declared") || states.includes("unreadable")) return { layers, verdict: "check layers, then test on arm64" };
return { layers, verdict: "candidate: test on arm64" };
}
async function scanRegion(region: string): Promise<{ rows: Row[]; total: number; arm: number }> {
const lambda = new LambdaClient({ region });
const cw = new CloudWatchClient({ region });
const x86: FunctionConfiguration[] = [];
let total = 0;
for await (const page of paginateListFunctions({ client: lambda }, {})) {
for (const fn of page.Functions ?? []) {
total++;
const arch = fn.Architectures?.[0] ?? "x86_64"; // functions created before arm64 existed may omit it
if (arch === "x86_64") x86.push(fn);
}
}
const used = await usage(cw, x86.map((f) => f.FunctionName ?? ""));
const monthFactor = 30 / days;
const rows: Row[] = [];
for (const fn of x86) {
const name = fn.FunctionName ?? "";
const u = used.get(name) ?? { durationMs: 0, invocations: 0 };
const gbSeconds = (u.durationMs / 1000) * ((fn.MemorySize ?? 128) / 1024);
const { layers, verdict } = await verdictFor(lambda, fn);
rows.push({
Region: region,
Function: name,
Runtime: fn.Runtime ?? "(image)",
Package: fn.PackageType ?? "Zip",
MemoryMB: fn.MemorySize ?? 128,
Layers: layers,
Invocations: Math.round(u.invocations),
GbSeconds: Math.round(gbSeconds),
X86PerMonth: `$${(gbSeconds * X86_PER_GB_S * monthFactor).toFixed(2)}`,
SavePerMonth: `$${(gbSeconds * (X86_PER_GB_S - ARM_PER_GB_S) * monthFactor).toFixed(2)}`,
Verdict: verdict,
});
}
return { rows, total, arm: total - x86.length };
}
function toCsv(rows: Row[]): string {
const cols = Object.keys(rows[0] ?? {}) as (keyof Row)[];
const cell = (v: string | number) => `"${String(v).replace(/"/g, '""')}"`;
return [cols.join(","), ...rows.map((r) => cols.map((c) => cell(r[c])).join(","))].join("\n") + "\n";
}
async function main(): Promise<void> {
if (!Number.isInteger(days) || days < 1 || days > 455) throw new Error("--days must be a whole number from 1 to 455");
const rows: Row[] = [];
let total = 0;
let arm = 0;
for (const region of regions) {
try {
const r = await scanRegion(region);
rows.push(...r.rows);
total += r.total;
arm += r.arm;
} catch (err) {
console.error(`${region}: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
}
}
rows.sort((a, b) => Number(b.SavePerMonth.slice(1)) - Number(a.SavePerMonth.slice(1)));
if (rows.length > 0) console.table(rows);
const save = rows.reduce((s, r) => s + Number(r.SavePerMonth.slice(1)), 0);
console.log(`${rows.length} of ${total} functions run on x86_64 (${arm} already on arm64). ` +
`Moving all of them would cut duration cost by about $${save.toFixed(2)} a month at the same duration (us-east-1 prices).`);
if (csvPath && rows.length > 0) {
writeFileSync(csvPath, toCsv(rows));
console.log(`Wrote ${rows.length} rows to ${csvPath}`);
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
How do you run it?
npm install @aws-sdk/client-lambda @aws-sdk/client-cloudwatch
npm install --save-dev tsx typescript @types/node
# Last 30 days in two Regions, with a CSV for the owning teams
AWS_PROFILE=readonly npx tsx find-lambda-functions-not-on-arm64.ts --regions us-east-1,eu-west-1 --csv arm64.csv
# A quieter month? Use 90 days so the estimate isn't skewed by one busy week
AWS_PROFILE=readonly npx tsx find-lambda-functions-not-on-arm64.ts --days 90
Sample output
┌─────────┬─────────────┬───────────────┬──────────────┬─────────┬──────────┬───────────────────────────┬─────────────┬───────────┬─────────────┬──────────────┬─────────────────────────────────────────┐
│ (index) │ Region │ Function │ Runtime │ Package │ MemoryMB │ Layers │ Invocations │ GbSeconds │ X86PerMonth │ SavePerMonth │ Verdict │
├─────────┼─────────────┼───────────────┼──────────────┼─────────┼──────────┼───────────────────────────┼─────────────┼───────────┼─────────────┼──────────────┼─────────────────────────────────────────┤
│ 0 │ 'us-east-1' │ 'orders-api' │ 'nodejs22.x' │ 'Zip' │ 1024 │ 'sharp-image: arm64 ok' │ 30000000 │ 3600000 │ '$60.00' │ '$12.00' │ 'candidate: test on arm64' │
│ 1 │ 'us-east-1' │ 'thumbnailer' │ 'python3.12' │ 'Zip' │ 2048 │ 'pillow-x86: x86_64 only' │ 2000000 │ 1800000 │ '$30.00' │ '$6.00' │ 'blocked: replace x86_64-only layer' │
│ 2 │ 'us-east-1' │ 'report-job' │ '(image)' │ 'Image' │ 3008 │ 'none' │ 120000 │ 1175000 │ '$19.58' │ '$3.92' │ 'rebuild the container image for arm64' │
│ 3 │ 'us-east-1' │ 'legacy-cron' │ 'nodejs16.x' │ 'Zip' │ 256 │ 'none' │ 8600 │ 250 │ '$0.00' │ '$0.00' │ 'upgrade the runtime first' │
└─────────┴─────────────┴───────────────┴──────────────┴─────────┴──────────┴───────────────────────────┴─────────────┴───────────┴─────────────┴──────────────┴─────────────────────────────────────────┘
4 of 5 functions run on x86_64 (1 already on arm64). Moving all of them would cut duration cost by about $21.92 a month at the same duration (us-east-1 prices).
Names and numbers are illustrative. orders-api is the first to move: a supported runtime, one layer with an arm64 build, and the biggest saving. thumbnailer depends on a Pillow layer built only for x86_64, so publish an arm64 build of that layer first. report-job is a container image and needs an arm64 build of the image. legacy-cron saves nothing worth the effort, but its runtime needs upgrading anyway. The script to get Lambda invocation counts for the last 24 hours helps when you want a closer look at one function’s traffic. Once the functions have moved, the x86_64-only layer versions are left unused, and the script to delete old Lambda layer versions cleans them up.
How do you migrate a function once it’s a candidate?
- Rebuild dependencies for arm64Install packages on an arm64 machine or in an arm64 container, for example
docker run --platform linux/arm64with the runtime’s base image. For Python wheels,pip install --platform manylinux2014_aarch64 --only-binary=:all: --target packagedownloads arm64 builds. - Publish arm64 layersRebuild each layer and set
CompatibleArchitecturestoarm64when you publish it, so the next scan sees it. - Deploy code and architecture togetherThe architecture is set with the code:
UpdateFunctionCodetakesArchitectures: ["arm64"], and in the console it’s under Runtime settings. In CDK, Terraform or SAM, change the function’s architecture property. - Split trafficThe Lambda Developer Guide suggests alias routing: publish the arm64 version and send a small weight of the alias to it.
- CompareCheck
Duration,Errorsand cold starts for both versions. The guide to investigate Lambda errors with CloudWatch has the queries. - Cut over and clean upMove the alias to 100% and later delete old and unused Lambda function versions.
Compute Savings Plans apply to Lambda too, so a lower bill also changes how much commitment you need; the script to check Savings Plans coverage and utilization shows where you stand. If you run EC2 as well, the scan to find previous-generation EC2 instances is the matching review for servers.
Troubleshooting
- Every layer shows “unreadable”. The profile lacks
lambda:GetLayerVersion, or the layers belong to another account. Check with the layer owner which architectures a version supports. - GbSeconds is 0 for a busy function. The function is in a Region you didn’t pass, or the window is shorter than its schedule. Re-run with
--days 90. - The arm64 version fails to load a module. A binary in the package or a layer is still x86_64. The function’s log shows which import failed; rebuild that dependency in an arm64 environment and redeploy.
- An access-denied error in one Region. An SCP may block that Region; the steps to troubleshoot AWS IAM access denied errors show how to read the message.
Ask ChatWithCloud instead
For a quick count, ask ChatWithCloud “Which of my Lambda functions still use the x86_64 architecture, and what runtimes are they on?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your profile and explains the result; how ChatWithCloud runs AWS SDK code locally shows each step. It uses one profile and Region per session and runs changes without asking first, so connect ChatWithCloud to a read-only AWS profile and leave the switch to your deployment pipeline. More scripts like this one are on the AWS practical examples hub.
Frequently asked questions
Is Lambda cheaper on arm64?
Yes, per GB-second. In us-east-1 the first-tier duration price is $0.0000133334 on arm64 against $0.0000166667 on x86_64 (September 2026), 20% less. Request prices are the same.
Can I change a Lambda function’s architecture without redeploying code?
No. The architecture is set together with the code, through UpdateFunctionCode with Architectures, the console’s Runtime settings, or your infrastructure-as-code tool. Code with native binaries must be rebuilt for arm64 first.
Do all Lambda runtimes support arm64?
All currently supported runtimes do, according to the Lambda Developer Guide. Functions on deprecated runtimes should be upgraded before you switch.
How do I check if a Lambda layer supports arm64?
Call GetLayerVersionByArn or aws lambda get-layer-version-by-arn --arn <layer-version-arn> and read CompatibleArchitectures. If it’s empty, the publisher didn’t declare it, and you need to test.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud