Photo by neil macc on Unsplash
To set CloudWatch log retention for all log groups that never expire, list them with DescribeLogGroups, keep the ones with no retentionInDays, and call PutRetentionPolicy on each with an allowed value such as 30 or 90 days. The script below does that in one region, shows stored GB and storage cost per group, and changes nothing unless you pass --apply.
CloudWatch Logs keeps log events forever unless a log group has a retention policy, and log groups that services create for you, such as the /aws/lambda/ group for every function, start with no retention. Storage then grows every month for as long as the account exists. This example is for engineers who want to set CloudWatch log retention for all log groups that never expire, in one pass, without touching groups that someone already configured on purpose.
It’s one of our AWS practical examples for cost cleanup. To see whether log storage is worth your time, run the script to get this month’s AWS CloudWatch cost with Cost Explorer first: log storage shows up there as the TimedStorage-ByteHrs usage type. The same “no expiry rule” problem affects container images, which you fix when you set an ECR lifecycle policy to delete old images.
Retention deletes data: once a retention policy is set, events older than the period are removed, typically within 72 hours. Export anything you must keep before you run with --apply. Without --apply the script only reads.
Which retention values can you set?
PutRetentionPolicy accepts only these retentionInDays values: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288 and 3653. Anything else fails with InvalidParameterException, so the script checks --days against the list before it calls AWS. To make a group never expire again, you’d call DeleteRetentionPolicy.
| Log type | Common choice | Why |
|---|---|---|
| Lambda and container application logs | 14–30 days | Long enough to debug last week’s incident |
| VPC Flow Logs, load balancer and API access logs | 30–90 days | Traffic investigations and trend comparisons |
| CloudTrail and security logs | 365 days or more | Often set by your audit or compliance policy |
These are starting points, not rules: your compliance requirements win. Use --prefix to apply different periods to different families, for example /aws/lambda/ at 30 days and /aws/vpc/ at 90.
What does the script do?
- Lists log groups
paginateDescribeLogGroups, optionally withlogGroupNamePrefix, 50 groups per page. - Keeps the ones that never expireA log group with no
retentionInDaysin the response keeps events indefinitely. Groups in theDELIVERYclass are skipped because they keep events for only one day anyway. - Prints a cost-ranked reportName, class, creation date, stored GB (from
storedBytes) and the monthly storage cost, largest first. - Applies only with
--applyCallsPutRetentionPolicyone group at a time with a short pause, logs any failure and carries on.
Prerequisites
- Node.js 18 or later, npm and
tsx. - The
@aws-sdk/client-cloudwatch-logspackage. Its API surface is documented in the client-cloudwatch-logs package in the AWS SDK for JavaScript v3 repository. - A profile with a default region, or
AWS_REGIONset. Log groups are regional, so run it once per region.
Which IAM permissions does it need?
The dry run needs logs:DescribeLogGroups. The second statement allows logs:PutRetentionPolicy on log groups in your account only (replace 123456789012); narrow the ARN to a prefix such as log-group:/aws/lambda/* if the profile should touch only some groups.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListLogGroups",
"Effect": "Allow",
"Action": "logs:DescribeLogGroups",
"Resource": "*"
},
{
"Sid": "SetRetention",
"Effect": "Allow",
"Action": "logs:PutRetentionPolicy",
"Resource": "arn:aws:logs:*:123456789012:log-group:*"
}
]
}
The free IAM policy generator for TypeScript code confirms these two actions if you paste the script in, and the guide to review an IAM policy for least privilege explains why the write action gets its own statement.
The full script to set CloudWatch log retention for all log groups
// set-log-retention.ts
// Finds CloudWatch Logs log groups with no retention ("Never expire") in one region and,
// with --apply, sets a retention period on them. Dry run by default.
// Usage: npx tsx set-log-retention.ts [--days 30] [--prefix /aws/lambda/] [--apply]
import {
CloudWatchLogsClient,
PutRetentionPolicyCommand,
paginateDescribeLogGroups,
type LogGroup,
} from "@aws-sdk/client-cloudwatch-logs";
// The only values PutRetentionPolicy accepts.
const ALLOWED = [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653];
// USD per GB-month of stored log data, us-east-1, as of September 2026. Other regions differ.
const STORAGE_PER_GB_MONTH = 0.03;
function arg(name: string): string | undefined {
const i = process.argv.indexOf(name);
return i === -1 ? undefined : process.argv[i + 1];
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function main(): Promise<void> {
const days = Number(arg("--days") ?? 30);
if (!ALLOWED.includes(days)) {
throw new Error(`--days must be one of: ${ALLOWED.join(", ")}`);
}
const prefix = arg("--prefix");
const apply = process.argv.includes("--apply");
const logs = new CloudWatchLogsClient({}); // region from AWS_REGION or your profile
const neverExpire: LogGroup[] = [];
let total = 0;
for await (const page of paginateDescribeLogGroups(
{ client: logs },
prefix ? { logGroupNamePrefix: prefix } : {},
)) {
for (const g of page.logGroups ?? []) {
total++;
// No retentionInDays means the group keeps events forever.
if (g.retentionInDays === undefined && g.logGroupClass !== "DELIVERY") neverExpire.push(g);
}
}
if (neverExpire.length === 0) {
console.log(`All ${total} log groups already have a retention period.`);
return;
}
neverExpire.sort((a, b) => (b.storedBytes ?? 0) - (a.storedBytes ?? 0));
const gb = (g: LogGroup) => (g.storedBytes ?? 0) / 1024 ** 3;
console.table(
neverExpire.map((g) => ({
"Log group": g.logGroupName,
Class: g.logGroupClass ?? "STANDARD",
Created: g.creationTime ? new Date(g.creationTime).toISOString().slice(0, 10) : "",
"Stored GB": gb(g).toFixed(2),
"Storage $/mo": (gb(g) * STORAGE_PER_GB_MONTH).toFixed(2),
})),
);
const totalGb = neverExpire.reduce((s, g) => s + gb(g), 0);
console.log(
`${neverExpire.length} of ${total} log groups never expire: ${totalGb.toFixed(1)} GB, ` +
`about $${(totalGb * STORAGE_PER_GB_MONTH).toFixed(2)}/month in storage and growing.`,
);
if (!apply) {
console.log(`Dry run. Re-run with --apply to set ${days}-day retention on these ${neverExpire.length} groups.`);
return;
}
let done = 0;
for (const g of neverExpire) {
const name = g.logGroupName ?? "";
try {
await logs.send(new PutRetentionPolicyCommand({ logGroupName: name, retentionInDays: days }));
done++;
console.log(`${name}: retention set to ${days} days`);
} catch (err) {
console.error(`${name}: ${(err as Error).name}: ${(err as Error).message}`);
}
await sleep(250); // stay well under the API rate limit on accounts with thousands of groups
}
console.log(`Done. ${done} of ${neverExpire.length} log groups updated. Older events are removed within about 72 hours.`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
How do you run it?
npm install @aws-sdk/client-cloudwatch-logs
npm install --save-dev tsx typescript
# 1. Dry run: which groups never expire, and what do they cost?
AWS_PROFILE=readonly AWS_REGION=us-east-1 npx tsx set-log-retention.ts
# 2. Lambda log groups: 30 days
AWS_PROFILE=ops AWS_REGION=us-east-1 npx tsx set-log-retention.ts --prefix /aws/lambda/ --days 30 --apply
# 3. Everything else that still never expires: 90 days
AWS_PROFILE=ops AWS_REGION=us-east-1 npx tsx set-log-retention.ts --days 90 --apply
Sample output
┌─────────┬──────────────────────────────────┬────────────┬──────────────┬───────────┬──────────────┐
│ (index) │ Log group │ Class │ Created │ Stored GB │ Storage $/mo │
├─────────┼──────────────────────────────────┼────────────┼──────────────┼───────────┼──────────────┤
│ 0 │ '/aws/vpc/flow-logs-prod' │ 'STANDARD' │ '2022-03-14' │ '1843.20' │ '55.30' │
│ 1 │ '/aws/lambda/orders-api' │ 'STANDARD' │ '2021-06-02' │ '412.75' │ '12.38' │
│ 2 │ '/ecs/checkout-service' │ 'STANDARD' │ '2023-01-19' │ '96.10' │ '2.88' │
│ 3 │ '/aws/lambda/nightly-report' │ 'STANDARD' │ '2024-08-30' │ '3.42' │ '0.10' │
│ 4 │ '/aws/lambda/test-hello-world' │ 'STANDARD' │ '2025-02-11' │ '0.00' │ '0.00' │
└─────────┴──────────────────────────────────┴────────────┴──────────────┴───────────┴──────────────┘
5 of 38 log groups never expire: 2355.5 GB, about $70.66/month in storage and growing.
Dry run. Re-run with --apply to set 30-day retention on these 5 groups.
Log group names and sizes are illustrative. Most of the cost usually sits in one or two groups, often flow logs or a chatty service, so review the top of the list before applying one period to everything. If flow logs top it, the script to find VPCs without flow logs and enable them compares CloudWatch Logs with the cheaper S3 destination.
How much does log retention save?
CloudWatch Logs bills stored data at $0.03 per GB-month in US East (N. Virginia), as of September 2026, on top of ingestion ($0.50 per GB for custom logs in the Standard class). Both rates are listed on the Amazon CloudWatch pricing page that the month-to-date CloudWatch cost example links to.
A worked example: a log group ingests 50 GB a month and has been doing so for 3 years with no retention. It stores about 36 × 50 = 1,800 GB, costing 1,800 × $0.03 = $54.00 a month, and adds $1.50 a month every month. With 30-day retention it holds about 50 GB: 50 × $0.03 = $1.50 a month. Retention doesn’t change ingestion cost, which is usually the bigger line; for that, lower log levels or sample verbose logs. The guide to investigate Lambda errors with CloudWatch helps you spot which functions log the most. Log groups you create for API Gateway access logs need retention from the start; the script to audit and enable API Gateway access logging sets it when it creates the group.
If you need logs for longer than your retention period, export them to S3 first and keep them in a cheaper storage class. The comparison of S3 storage class costs for backups shows which class suits long-lived archives.
Troubleshooting
InvalidParameterException.retentionInDaysisn’t one of the allowed values. The script refuses such values before calling AWS; check any other tooling that sets retention.AccessDeniedExceptiononPutRetentionPolicy. The profile lacks the second statement, or its ARN doesn’t cover the group. The steps to troubleshoot IAM access denied errors apply.- Retention comes back to Never expire. Infrastructure as code or a deployment tool is recreating the group without retention. Set retention in the template, for example
RetentionInDaysonAWS::Logs::LogGroup. - Stored GB didn’t drop right away. Expired events are typically deleted within 72 hours, sometimes longer, and they stop counting toward storage once they’re marked for deletion.
Ask ChatWithCloud instead
To explore first, ask ChatWithCloud “Which CloudWatch log groups have no retention, and which are biggest?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your profile and summarizes the result, much like the checks in the guide to troubleshoot AWS infrastructure with an AI CLI. Leave the change to the script: ChatWithCloud runs generated code without a confirmation step, and retention deletes data. Connect ChatWithCloud with a read-only AWS profile, and see how ChatWithCloud turns a question into AWS SDK calls.
Frequently asked questions
What is the default CloudWatch log retention?
Never expire. Log groups keep events indefinitely until you set a retention policy.
How do I set log retention for all log groups with the AWS CLI?
List groups with aws logs describe-log-groups --query "logGroups[?!retentionInDays].logGroupName", then run aws logs put-retention-policy --log-group-name NAME --retention-in-days 30 for each.
Does changing retention delete logs immediately?
No. Events past the new period are marked for deletion and typically removed within 72 hours. They stop adding to storage cost once marked.
Can new log groups get a retention period automatically?
Not from this script. Set retention wherever groups are created, in your templates, or run the script on a schedule to catch new groups.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud