The note on what survives a VM has a serverless cousin: the log group. When a Lambda function is invoked for the first time, CloudWatch creates /aws/lambda/<name> with "Never expire" retention, and when you delete the function that log group does not go with it. The Lambda documentation says it in one line: "Log groups aren't deleted automatically when you delete a function". On November 9 AWS published account-level retention policies, the first way to put an expiry on all of them at once. Everything below comes from docs.aws.amazon.com and the pricing page; the sources are at the end.
Why it stays at "Never expire"
CloudWatch keeps log events indefinitely unless someone sets a retention; in the console that state is labelled "Never Expire". Lambda creates the log group for you, on the first invocation, with that value. A Terraform stack that declares the function but not the log group leaves a resource Terraform never had in state: terraform destroy removes the function and the log group stays, with its bytes and its line on the bill. It is the same mechanism as a disk created outside the plan.
Finding them: no retention, sorted by bytes
describe-log-groups returns retentionInDays only when there is a retention; if the group never expires, the field is absent. That lets you filter them with JMESPath by comparing against null, and storedBytes tells you how much each one weighs:
# log groups with no retention, heaviest first (CLI v2 paginates by itself) aws logs describe-log-groups --output table \ --query "reverse(sort_by(logGroups[?retentionInDays==null].{name:logGroupName,storedBytes:storedBytes,class:logGroupClass},&storedBytes))" # Lambda ones only aws logs describe-log-groups --log-group-name-prefix /aws/lambda \ --query "logGroups[?retentionInDays==null].[logGroupName,storedBytes]" --output text # functions that exist today, to cross against the list above aws lambda list-functions --query "Functions[].FunctionName" --output text A /aws/lambda/x log group with no function x in the second list is an orphan: the function was deleted and the logs stayed. The ones that do have a function but no retention are the other problem: they grow every month.
What it costs
In us-east-1, the Standard class charges US$ 0.50 per GB ingested and US$ 0.03 per GB-month stored (compressed); the Infrequent Access class, US$ 0.25 per GB ingested and US$ 0.018 per GB-month. The free tier covers 5 GB. Storage is what accumulates: 100 GB nobody reads is US$ 3 a month forever, and a noisy function that logs every request can leave several hundred GB before anyone looks.
Retention per log group
# one log group aws logs put-retention-policy --log-group-name /aws/lambda/my-function --retention-in-days 30 # every Lambda log group without retention, to 30 days for g in $(aws logs describe-log-groups --log-group-name-prefix /aws/lambda \ --query "logGroups[?retentionInDays==null].logGroupName" --output text); do aws logs put-retention-policy --log-group-name "$g" --retention-in-days 30 done The accepted values are fixed: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288 and 3653 days. Expired events can take up to 72 hours to be deleted. In Terraform, an aws_cloudwatch_log_group with retention_in_days and the name /aws/lambda/<function> declared before the function keeps the group in state, and skip_destroy = true preserves it on purpose through a destroy.
Account-level retention: what is new since November 9
An account-level retention policy sets one retention for every log group in the region or for a subset by name prefix, and chooses whether it applies to existing groups or only to new ones. For the Lambda ones:
aws logs put-account-policy \ --policy-name LambdaRetentionPolicy \ --policy-type RETENTION_POLICY \ --policy-document '{"retentionInDays": 90, "enforcementMode": "RETROACTIVE"}' \ --selection-criteria 'LogGroupNamePrefix="/aws/lambda"' # --dry-run shows which groups it would apply to without creating anything -
enforcementMode:RETROACTIVEreaches the groups that already exist and the new ones;NEW_ONLY, only those created afterwards. - The minimum is 30 days; the maximum, 3653, or
NEVER_EXPIRE. Up to 20 policies per region, and when several match, the longest retention wins. - The account policy replaces the log group's own retention, even when the group's was longer. It takes 24 hours to apply.
- No additional cost. Not available in Bahrain or the UAE.
What the policy does not do is delete the log group: an orphan with 90-day retention stops accumulating bytes and weighs zero in three months, but it is still listed. For it to disappear you need aws logs delete-log-group, and that is best done from the crossed list above, not with a for over everything under /aws/lambda/.
What Control Cloud does with this
Control Cloud's AWS inventory reads instances, volumes, elastic addresses, network interfaces, NAT gateways, load balancers, RDS, EKS, VPCs and subnets through the API, with a read-only credential, and flags what was left without an owner after a delete. It does not read Lambda functions or CloudWatch log groups yet: today, that is what the commands in this note are for. If you want to see the inventory on your account, request access; to connect it with read permissions, see the AWS guide.
Sources
- AWS Lambda Developer Guide, "Working with the Lambda logging and log groups" ("By default, CloudWatch automatically creates a log group named /aws/lambda/…") and "Logging in Node.js" ("Log groups aren't deleted automatically when you delete a function").
- Amazon CloudWatch Logs User Guide, "Working with log groups and log streams" (indefinite retention by default, "Never Expire", account-level retention policies with the
put-account-policyexample). - AWS What's New, November 9, 2026, "Amazon CloudWatch Logs now supports account-level log retention policies"; CLI reference
logs put-account-policy(RETROACTIVE / NEW_ONLY, 30-day minimum, 20 policies, 24 hours, replaces the group's). - CLI reference:
logs put-retention-policy(accepted values, deletion up to 72 hours later) andlogs describe-log-groups(retentionInDaysabsent when never expire,storedBytes). - Amazon CloudWatch Pricing, US East (N. Virginia): US$ 0.50 and US$ 0.25 per GB ingested, US$ 0.03 and US$ 0.018 per GB-month, 5 GB free.
- Terraform Registry,
aws_cloudwatch_log_group(retention_in_days,skip_destroy); hashicorp/terraform-provider-aws, issue #454 ("When you delete the lambda, it doesn't delete anything from CloudWatch").