Photo by MIKE STOLL on Unsplash
An EFS lifecycle policy moves files you haven’t read for a set time from EFS Standard into the cheaper Infrequent Access (IA) and Archive storage classes. To find file systems without one, call DescribeFileSystems, then DescribeLifecycleConfiguration for each; an empty LifecyclePolicies list means everything stays in Standard. The script below reports that, prices it and sets a policy behind --apply.
EFS Standard is one of the more expensive places to keep a file in AWS, and shared file systems collect cold data: home directories of people who left, build caches, old exports. Without an EFS lifecycle policy, all of it is billed at the Standard rate forever. Lifecycle management is a separate setting from the file system itself, so file systems created by scripts, Terraform or older templates often have none.
This example is for platform and FinOps engineers who want a per-file-system list of what’s in each storage class and a controlled way to fix the gaps. It’s the EFS counterpart of the script to find S3 buckets without lifecycle rules.
How does an EFS lifecycle policy work?
A file system has one lifecycle configuration made of up to three policies, each with a single transition:
| Policy | What it does | Values |
|---|---|---|
TransitionToIA |
Moves files not accessed in Standard for that long into IA | AFTER_1_DAY, AFTER_7_DAYS, 14, 30, 60, 90, 180, 270 or 365 days |
TransitionToArchive |
Moves files from Standard or IA into Archive; must be later than the IA transition | Same values as IA |
TransitionToPrimaryStorageClass |
Moves a file back to Standard when it’s read in IA or Archive | AFTER_1_ACCESS, or unset to leave files where they are |
The timer is internal: it resets whenever a file is accessed in Standard, and listing a directory doesn’t count as access. Metadata such as file names and directory structure always stays in Standard. Archive only works on file systems with Elastic throughput and General Purpose performance mode, so the script skips the Archive policy elsewhere. The EFS lifecycle management documentation describes the defaults: 30 days to IA, 90 days to Archive and no move back to Standard.
What do the storage classes cost?
As of September 2026, the AWS Price List for Amazon EFS shows these us-east-1 rates:
| Storage class | Storage per GB-month | Access charge |
|---|---|---|
| Standard (Regional) | $0.30 | None for storage; throughput mode charges apply |
| Infrequent Access | $0.016 with Elastic throughput, $0.025 otherwise | $0.01 per GB read, $0.01 per GB written |
| Archive | $0.008 | $0.03 per GB read, $0.03 per GB written or tiered |
| One Zone / One Zone-IA | $0.16 / $0.0133 | See the EFS pricing page |
IA and Archive also bill every file as at least 128 KiB, so a file system of tiny files saves less than the rates suggest. Worked example: an 820 GiB shared home directory on Elastic throughput costs 820 × $0.30 = $246.00 a month in Standard. If 574 GiB of it hasn’t been touched in 30 days, moving that to IA saves 574 × ($0.30 − $0.016) = $163.02 a month, less $0.01 per GB whenever someone reads it back. The script treats GiB as GB for these estimates, which slightly overstates cost.
What does the script do?
- Lists file systems
paginateDescribeFileSystemsper Region, with throughput mode, performance mode and whether it’s One Zone. - Reads each lifecycle configuration
DescribeLifecycleConfiguration; an empty list means no policy. - Reports size by class
SizeInByteshasValueInStandard,ValueInIAandValueInArchive. These are metered sizes, eventually consistent, not a snapshot. - Prices itMonthly storage at today’s split, plus a ceiling saving if all Standard data were cold. The real number depends on your access pattern.
- Sets a policy only with
--applyPutLifecycleConfigurationon file systems with no policy at all, one transition per policy object. It never touches an existing configuration, because a put replaces it.
Prerequisites
- Node.js 18 or later, npm and
tsx, plus the@aws-sdk/client-efspackage. - A read-only profile for the report and a separate one for
--apply. - For encrypted file systems, the same KMS permissions that were needed to create them, or the put fails.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReportLifecycle",
"Effect": "Allow",
"Action": [
"elasticfilesystem:DescribeFileSystems",
"elasticfilesystem:DescribeLifecycleConfiguration"
],
"Resource": "*"
},
{
"Sid": "SetLifecycleOnlyWithApply",
"Effect": "Allow",
"Action": "elasticfilesystem:PutLifecycleConfiguration",
"Resource": "arn:aws:elasticfilesystem:*:123456789012:file-system/*"
}
]
}
Give the second statement only to the role that runs --apply. To derive the list from the code, use the IAM policy generator for TypeScript code; if a call is denied, the guide to troubleshoot AWS IAM access denied errors shows how to read the message.
The script: EFS lifecycle policy report and fix
// find-efs-without-lifecycle-policy.ts
// Lists every EFS file system with its lifecycle policy (TransitionToIA, TransitionToArchive,
// TransitionToPrimaryStorageClass), its metered size per storage class and a monthly storage estimate.
// Read-only by default. With --apply it sets a lifecycle policy on file systems that have none.
// Usage:
// npx tsx find-efs-without-lifecycle-policy.ts [--regions us-east-1,eu-west-1] [--csv efs.csv]
// npx tsx find-efs-without-lifecycle-policy.ts --ia AFTER_30_DAYS [--archive AFTER_90_DAYS] [--back-on-access] [--apply]
import { writeFileSync } from "node:fs";
import {
DescribeLifecycleConfigurationCommand,
EFSClient,
paginateDescribeFileSystems,
PutLifecycleConfigurationCommand,
type FileSystemDescription,
type LifecyclePolicy,
type TransitionToArchiveRules,
type TransitionToIARules,
} from "@aws-sdk/client-efs";
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 csvPath = flag("--csv");
const apply = args.includes("--apply");
const backOnAccess = args.includes("--back-on-access");
const RULES = ["AFTER_1_DAY", "AFTER_7_DAYS", "AFTER_14_DAYS", "AFTER_30_DAYS", "AFTER_60_DAYS", "AFTER_90_DAYS", "AFTER_180_DAYS", "AFTER_270_DAYS", "AFTER_365_DAYS"] as const;
const daysOf = (rule: string) => Number(rule.match(/\d+/)?.[0] ?? "0");
const isRule = (v: string | undefined): v is (typeof RULES)[number] => RULES.some((r) => r === v);
const iaArg = flag("--ia") ?? "AFTER_30_DAYS";
const archiveArg = flag("--archive");
if (!isRule(iaArg)) throw new Error(`--ia must be one of ${RULES.join(", ")}`);
if (archiveArg !== undefined && (!isRule(archiveArg) || daysOf(archiveArg) <= daysOf(iaArg))) {
throw new Error("--archive must be a valid rule and later than --ia");
}
const ia: TransitionToIARules = iaArg;
const archive: TransitionToArchiveRules | undefined = archiveArg;
// us-east-1 list prices per GB-month from the AWS Price List API, checked September 2026. Edit for other Regions.
const PRICE = {
standard: 0.3,
iaElastic: 0.016, // IA on file systems with Elastic throughput
iaOther: 0.025, // IA with Bursting or Provisioned throughput
archive: 0.008,
oneZoneStandard: 0.16,
oneZoneIa: 0.0133,
};
const GIB = 1024 ** 3;
interface Row {
Region: string;
FileSystem: string;
Name: string;
Throughput: string;
Standard_GiB: number;
IA_GiB: number;
Archive_GiB: number;
Policy: string;
Monthly: string;
MaxSaving: string; // ceiling: every Standard byte moved to IA
Action: string;
}
function describePolicy(policies: LifecyclePolicy[]): string {
const parts = policies.flatMap((p) => [
p.TransitionToIA ? `IA ${p.TransitionToIA}` : "",
p.TransitionToArchive ? `Archive ${p.TransitionToArchive}` : "",
p.TransitionToPrimaryStorageClass ? `back ${p.TransitionToPrimaryStorageClass}` : "",
]);
return parts.filter(Boolean).join(", ") || "none";
}
function desiredPolicies(fs: FileSystemDescription): LifecyclePolicy[] {
const policies: LifecyclePolicy[] = [{ TransitionToIA: ia }]; // one transition per LifecyclePolicy object
// Archive needs Elastic throughput and General Purpose performance mode.
if (archive && fs.ThroughputMode === "elastic" && fs.PerformanceMode === "generalPurpose") policies.push({ TransitionToArchive: archive });
if (backOnAccess) policies.push({ TransitionToPrimaryStorageClass: "AFTER_1_ACCESS" });
return policies;
}
async function scanRegion(region: string): Promise<Row[]> {
const efs = new EFSClient({ region });
const rows: Row[] = [];
for await (const page of paginateDescribeFileSystems({ client: efs }, {})) {
for (const fs of page.FileSystems ?? []) {
const id = fs.FileSystemId ?? "";
const size = fs.SizeInBytes;
const std = (size?.ValueInStandard ?? size?.Value ?? 0) / GIB;
const inIa = (size?.ValueInIA ?? 0) / GIB;
const inArchive = (size?.ValueInArchive ?? 0) / GIB;
const oneZone = Boolean(fs.AvailabilityZoneName);
const stdRate = oneZone ? PRICE.oneZoneStandard : PRICE.standard;
const iaRate = oneZone ? PRICE.oneZoneIa : fs.ThroughputMode === "elastic" ? PRICE.iaElastic : PRICE.iaOther;
const { LifecyclePolicies = [] } = await efs.send(new DescribeLifecycleConfigurationCommand({ FileSystemId: id }));
const hasIa = LifecyclePolicies.some((p) => p.TransitionToIA);
const row: Row = {
Region: region,
FileSystem: id,
Name: fs.Name ?? "",
Throughput: `${fs.ThroughputMode ?? "?"}${oneZone ? " (One Zone)" : ""}`,
Standard_GiB: Math.round(std * 10) / 10,
IA_GiB: Math.round(inIa * 10) / 10,
Archive_GiB: Math.round(inArchive * 10) / 10,
Policy: describePolicy(LifecyclePolicies),
Monthly: `$${(std * stdRate + inIa * iaRate + inArchive * PRICE.archive).toFixed(2)}`,
MaxSaving: hasIa ? "" : `$${(std * (stdRate - iaRate)).toFixed(2)}`,
Action: "",
};
rows.push(row);
// Only file systems with no lifecycle policy at all: Put replaces the whole configuration.
if (LifecyclePolicies.length) continue;
const wanted = desiredPolicies(fs);
if (!apply) {
row.Action = `would set ${describePolicy(wanted)}`;
} else if (fs.LifeCycleState !== "available") {
row.Action = `skipped: ${fs.LifeCycleState}`;
} else {
try {
await efs.send(new PutLifecycleConfigurationCommand({ FileSystemId: id, LifecyclePolicies: wanted }));
row.Action = `set ${describePolicy(wanted)}`;
} catch (err) {
row.Action = `failed: ${err instanceof Error ? err.name : String(err)}`;
}
}
}
}
return rows;
}
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> {
const rows: Row[] = [];
for (const region of regions) rows.push(...(await scanRegion(region)));
console.table(rows);
const none = rows.filter((r) => r.Policy === "none");
const ceiling = none.reduce((n, r) => n + Number(r.MaxSaving.replace(/[^0-9.]/g, "") || 0), 0);
console.log(`${rows.length} file systems, ${none.length} without a lifecycle policy`);
console.log(`Upper bound if all their Standard data were cold: $${ceiling.toFixed(2)} a month`);
if (csvPath) {
writeFileSync(csvPath, toCsv(rows));
console.log(`Wrote ${rows.length} rows to ${csvPath}`);
}
if (!apply) console.log("Dry run: no lifecycle policy was changed. Add --apply to set them.");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
How do you run it?
npm install @aws-sdk/client-efs
npm install --save-dev tsx typescript @types/node
# Report only, with a CSV
AWS_PROFILE=readonly npx tsx find-efs-without-lifecycle-policy.ts --regions us-east-1,eu-west-1 --csv efs.csv
# Preview: IA after 30 days, Archive after 90 where supported
AWS_PROFILE=storage-admin npx tsx find-efs-without-lifecycle-policy.ts --ia AFTER_30_DAYS --archive AFTER_90_DAYS
# Apply it
AWS_PROFILE=storage-admin npx tsx find-efs-without-lifecycle-policy.ts --ia AFTER_30_DAYS --archive AFTER_90_DAYS --apply
Sample output
┌─────────┬─────────────┬──────────────────┬─────────────────┬────────────────────────┬──────────────┬────────┬─────────────┬─────────────────────────────────────┬───────────┬───────────┬─────────────────────────────────────────────────────┐
│ (index) │ Region │ FileSystem │ Name │ Throughput │ Standard_GiB │ IA_GiB │ Archive_GiB │ Policy │ Monthly │ MaxSaving │ Action │
├─────────┼─────────────┼──────────────────┼─────────────────┼────────────────────────┼──────────────┼────────┼─────────────┼─────────────────────────────────────┼───────────┼───────────┼─────────────────────────────────────────────────────┤
│ 0 │ 'us-east-1' │ 'fs-0a1b2c3d4e5' │ 'shared-home' │ 'elastic' │ 820 │ 0 │ 0 │ 'none' │ '$246.00' │ '$232.88' │ 'would set IA AFTER_30_DAYS, Archive AFTER_90_DAYS' │
│ 1 │ 'us-east-1' │ 'fs-0b2c3d4e5f6' │ 'ci-cache' │ 'bursting' │ 140.5 │ 0 │ 0 │ 'none' │ '$42.15' │ '$38.64' │ 'would set IA AFTER_30_DAYS' │
│ 2 │ 'us-east-1' │ 'fs-0c3d4e5f607' │ 'media-archive' │ 'elastic' │ 95.2 │ 1210 │ 3400 │ 'IA AFTER_30_DAYS, Archive AFTER_90_DAYS' │ '$75.12' │ '' │ '' │
│ 3 │ 'eu-west-1' │ 'fs-0d4e5f60718' │ 'dev-scratch' │ 'bursting (One Zone)' │ 12.4 │ 0 │ 0 │ 'none' │ '$1.98' │ '$1.82' │ 'would set IA AFTER_30_DAYS' │
└─────────┴─────────────┴──────────────────┴─────────────────┴────────────────────────┴──────────────┴────────┴─────────────┴─────────────────────────────────────┴───────────┴───────────┴─────────────────────────────────────────────────────┘
4 file systems, 3 without a lifecycle policy
Upper bound if all their Standard data were cold: $273.34 a month
Dry run: no lifecycle policy was changed. Add --apply to set them.
IDs are illustrative. media-archive shows what a working policy looks like: 4.6 TiB stored for about $75 a month. ci-cache uses Bursting throughput, so it gets IA but not Archive.
Which EFS lifecycle policy should each file system get?
- Home directories and shared team data: IA after 30 days, Archive after 90 or 180. Most files are written once and rarely opened again.
- Build caches and scratch space: IA after 7 or 14 days, and consider deleting old data instead; no lifecycle beats not storing it.
- Latency-sensitive apps with many small files: add
TransitionToPrimaryStorageClass: AFTER_1_ACCESSso files that become hot again return to Standard, or keep the file system out of the policy entirely. - Data read in full every day, such as ML training sets: no policy. Every read from IA or Archive is billed per GB, which can cost more than the storage saved.
The trade-off mirrors S3, and the comparison of S3 Standard vs Intelligent-Tiering costs and the guide to choosing an S3 storage class for backups walk through the same access-versus-storage arithmetic. After a month, confirm the change on the bill with the script to fetch AWS cost and usage data by service. Block storage has its own version of paying for capacity nobody uses: find overprovisioned EBS IOPS on io1 and io2 volumes.
Troubleshooting
- An error when setting Archive. The file system uses Bursting or Provisioned throughput, or Max I/O performance mode. The script already skips Archive there; for other cases, switch to Elastic throughput first.
IncorrectFileSystemLifeCycleState. The file system isn’tavailable, for example while it’s being created or deleted. Re-run later.- Sizes don’t change right after
--apply. Transitions run at lower priority than your workload and start only once files pass the age threshold. Millions of small files take longer than a few large ones. - The bill barely moved. Check for small files, which are billed as 128 KiB in IA and Archive, and for applications that read the whole tree every night, which resets the timer.
Ask ChatWithCloud instead
For a quick answer in one Region, ask ChatWithCloud “Which EFS file systems in us-east-1 have no lifecycle policy, and how much data is in Standard?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your profile and explains the result, as described on how ChatWithCloud runs AWS SDK code locally. It uses one profile and Region per session and runs changes without a confirmation step, so connect ChatWithCloud to a read-only AWS profile and keep the --apply step in the script.
Frequently asked questions
How do I check the EFS lifecycle policy with the AWS CLI?
Run aws efs describe-lifecycle-configuration --file-system-id fs-0123456789abcdef0. An empty LifecyclePolicies array means no files ever leave Standard.
How do I remove an EFS lifecycle policy?
Call PutLifecycleConfiguration with an empty LifecyclePolicies array, or run aws efs put-lifecycle-configuration --file-system-id fs-0123456789abcdef0 --lifecycle-policies "[]". That deletes the whole configuration.
Does moving files to EFS IA change the path or break applications?
No. The file stays at the same path in the same file system; only the storage class behind it changes. Reads from IA and Archive are billed per GB, and Standard gives the lowest latency.
Why can’t I set Archive on my file system?
Archive requires Elastic throughput and General Purpose performance mode. Change the throughput mode first, then add TransitionToArchive.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud