CloudWatch Logs to S3: Where the Savings Are

Moving CloudWatch Logs to S3 only pays off if you skip ingestion. Compare us-east-1 prices, the Infrequent Access class, retention, Firehose and Athena.

Someone notices CloudWatch is the third-biggest line on the bill, opens a ticket called “move logs to S3”, and a sprint later there’s a tidy subscription filter pushing every log group into a bucket. The next invoice is about the same. Occasionally it’s a little higher.

The reason is in the price list. CloudWatch Logs charges mostly for getting data in, much less for keeping it, so a pipeline that copies logs out after they’ve landed has already paid the expensive part. S3 does save money, but only in a few specific places, and “export everything” isn’t one of them. Here’s where they are, and what each move costs you in features.

What CloudWatch Logs actually charges for

All prices here are on-demand list prices for US East (N. Virginia), us-east-1, at the time of writing (September 2026). Other regions differ; check the CloudWatch pricing page.

Charge us-east-1 list price
Ingestion, Standard log class $0.50 per GB
Ingestion, Infrequent Access log class $0.25 per GB
Storage (archived log data) $0.03 per GB-month
Logs Insights queries $0.005 per GB scanned
Vended logs (VPC Flow Logs etc.) into CloudWatch, Standard $0.50 per GB, first 10 TB (tiers down after)
Vended logs delivered straight to S3 or Firehose $0.25 per GB, first 10 TB (tiers down after)

Ingesting a gigabyte costs about seventeen times what storing it costs for a month, and AWS’s own pricing examples assume logs compress several-fold once stored. In almost every account we look at, ingestion is most of the CloudWatch Logs bill. Storage only gets big when retention has sat on the default for years.

Finding where the money goes

In Cost Explorer, group CloudWatch by usage type: DataProcessing-Bytes is log ingestion, VendedLog-Bytes vended ingestion, TimedStorage-ByteHrs storage. Then find the noisy groups:

# Read-only. Top 20 log groups by bytes ingested over the last 7 days
# (GNU date first, BSD/macOS date as the fallback)
SINCE=$(date -u -d '-7 days' +%FT%TZ 2>/dev/null || date -u -v-7d +%FT%TZ)
NOW=$(date -u +%FT%TZ)
for lg in $(aws logs describe-log-groups --query 'logGroups[].logGroupName' --output text); do
  bytes=$(aws cloudwatch get-metric-statistics --namespace AWS/Logs \
    --metric-name IncomingBytes --dimensions Name=LogGroupName,Value="$lg" \
    --start-time "$SINCE" --end-time "$NOW" \
    --period 604800 --statistics Sum --query 'Datapoints[0].Sum' --output text)
  echo "$bytes $lg"
done | sort -rn | head -20

It’s usually a short list: one service logging full request bodies at INFO, a debug flag left on since March, and VPC Flow Logs for every VPC.

Retention: the cheapest fix, and the one people skip

New log groups default to Never expire. Lambda and other services create log groups for you, so an account that has deployed a few hundred functions is quietly paying $0.03 per GB-month for every line they ever wrote, including functions deleted years ago.

# Read-only. Log groups with no retention set, biggest stored size first
aws logs describe-log-groups \
  --query 'reverse(sort_by(logGroups[?retentionInDays==null], &storedBytes))[].[logGroupName, storedBytes, logGroupClass]' \
  --output table
# CHANGES DATA: CloudWatch will delete events older than 30 days in this group
aws logs put-retention-policy --log-group-name /aws/lambda/orders-api --retention-in-days 30

Retention can be anything from one day to ten years. Agree it per group with the owner rather than with a blanket script, because somewhere in that list is a group an auditor expects to see in eighteen months. For those, the question is where the old data lives, which is where S3 earns its place.

The Infrequent Access log class

The Infrequent Access (IA) class halves ingestion to $0.25 per GB. Storage and Logs Insights cost the same as Standard. The catch is what it leaves out. According to the log classes documentation, IA log groups don’t support:

  • metric filters, so no log-derived metrics and no alarms built on them
  • subscription filters, so no streaming to Firehose, Lambda or OpenSearch
  • Live Tail, anomaly detection, and the embedded metric format
  • GetLogEvents and FilterLogEvents, so you read events through Logs Insights rather than scrolling a stream

The class is fixed at creation, so moving means a new group and repointing the writer (Lambda accepts a custom log group in its logging configuration).

# Creates a new, empty log group in the IA class
aws logs create-log-group --log-group-name /app/checkout/debug --log-group-class INFREQUENT_ACCESS

IA is a good home for high-volume logs you only query when something has already gone wrong. It’s a bad home for anything that feeds an alarm or a pipeline.

Four ways to get logs into S3, and what each one saves

1. CreateExportTask, for one-off backfills

Export tasks copy a time range of a log group into a bucket as gzip files. An account can run only one active export task at a time, data can take up to 12 hours to become exportable, and a task times out after 24 hours. AWS’s export documentation says plainly not to use them for continuous archiving. Where they shine is draining a huge Never-expire group before you shorten its retention.

# Writes objects to S3; deletes nothing in CloudWatch. Times are epoch milliseconds (calendar year 2025 here)
aws logs create-export-task \
  --log-group-name /app/orders \
  --from 1735689600000 --to 1767225600000 \
  --destination my-log-archive-bucket \
  --destination-prefix cloudwatch/app-orders

The bucket policy must let the CloudWatch Logs service principal write, and normal S3 charges apply.

2. Subscription filter to Firehose to S3

This is the pipeline most teams build. It’s near real time, but notice the order of events: the data is ingested into CloudWatch at full price and then copied out. Firehose adds its own per-GB ingestion fee, plus $0.00325 per GB if you have it decompress CloudWatch’s gzip payloads and $0.018 per GB for Parquet conversion.

What it buys you is the ability to cut CloudWatch retention to a week or two and keep the long tail in cheaper storage. That saves storage, usually the small number. It also won’t work on IA groups.

3. Vended logs straight to S3

This is where the easy money is. VPC Flow Logs, Route 53 Resolver query logs, AWS WAF logs, CloudFront standard logs and a long list of other vended logs can be delivered to S3 without touching CloudWatch at all. In us-east-1 that’s $0.25 per GB for the first 10 TB, against $0.50 per GB into a Standard CloudWatch log group. Flow logs can be written as Parquet for an extra $0.03 per GB, which makes later Athena queries much cheaper. ALB and NLB access logs already go to S3, and the ELB docs say there’s no charge for them beyond the storage.

# Read-only. Flow logs still delivering to CloudWatch Logs
aws ec2 describe-flow-logs \
  --query 'FlowLogs[?LogDestinationType==`cloud-watch-logs`].[FlowLogId,ResourceId,LogGroupName,TrafficType]' \
  --output table

Create the S3 flow log first, confirm objects arrive, then delete the CloudWatch one (a destructive change, so not the other way round). If you turned flow logs on to chase a NAT bill, our post on NAT Gateway and data transfer costs covers what to look for in them.

4. Don’t send application logs to CloudWatch at all

Fluent Bit (and FireLens on ECS) can ship straight to Firehose, which writes compressed batches to S3. Firehose Direct PUT ingestion is $0.029 per GB in us-east-1 at the first tier, but each record is rounded up to the next 5 KB. Pack many log lines into one record or a stream of 300-byte lines gets billed as if every one were 5 KB.

The split we usually recommend: keep warnings, errors and anything an alarm depends on in a Standard group, and send verbose INFO and DEBUG to Firehose.

A worked example: 1 TB of application logs a month

Assumptions, so you can swap in your own: one service writes 1,000 GB of logs a month in us-east-1, you keep 12 months, logs compress to roughly a fifth of raw size (in line with AWS’s pricing example; yours will vary), steady state after year one. Free tier, requests and queries are left out. S3 lifecycle moves objects to Glacier Instant Retrieval ($0.004 per GB-month) after 30 days, with the newest month in S3 Standard ($0.023).

Setup Monthly cost What you keep
A. CloudWatch Standard, 12-month retention $500 ingestion + $72 storage = $572 Everything
B. Standard, 14-day retention, subscription to Firehose to S3 $500 + ~$3 + up to $29 Firehose + $13.40 S3 = **$545** Everything; older logs via Athena
C. Infrequent Access class, 12-month retention $250 + $72 = $322 Insights, but no metric filters or subscriptions
D. App ships to Firehose, S3 only $29 + $13.40 = ~$42 Athena only
E. 10% (errors, alarm sources) in Standard with 30-day retention, 90% to Firehose and S3 $50.60 + $26.10 + $12.06 = ~$89 Alarms on what matters, Athena for the rest

Setup B, the one people usually build, saves about 5%. Setup C saves 44% with one CLI flag and a redeploy. Setup E is where we end up with most teams: roughly an 84% cut without giving up alerting.

And if A had been left on Never expire, storage would grow by about $6 a month, every month, forever. Three years in, it’s $216 a month for logs nobody reads.

For logs you hope never to read, Glacier Flexible Retrieval ($0.0036) and Deep Archive ($0.00099) are cheaper still, with 90- and 180-day minimums and restores measured in hours. Our guide to S3 storage classes and lifecycle rules walks through choosing between them.

Querying logs in S3 with Athena

Athena costs $5 per TB scanned, which is the same $0.005 per GB as Logs Insights. The difference is what gets scanned. Athena bills on the bytes it actually reads after compression, and with Parquet it reads only the columns a query uses, so Parquet plus date-based partitions (partition projection saves you from running crawlers) means a query for one hour on one day touches a sliver of the data. Each query has a 10 MB minimum charge.

Objects in Glacier Instant Retrieval are queryable as normal; Flexible Retrieval and Deep Archive objects must be restored first. And somebody has to own the Glue table definitions, because the first time a schema changes at 2 a.m. you’ll find out who that was.

What you give up by leaving CloudWatch

  • Logs Insights only queries CloudWatch log groups. Athena is slower to iterate with during an incident.
  • Metric filters and alarms. Filters feed custom metrics ($0.30 per metric-month for the first 10,000) and alarms ($0.10 per standard-resolution alarm metric). Keep alert-relevant lines in a Standard group, or emit metrics directly.
  • Live Tail, and freshness: Firehose buffers before writing, so S3 lags by minutes.
  • Plumbing. Firehose streams, IAM roles, bucket policies, lifecycle rules and Glue tables all need an owner, and the console’s “View logs” links stop helping.

When not to bother: below a few tens of GB a month. Twenty GB of Standard ingestion is $10 a month, which won’t repay the engineering time.

FAQ

Is it cheaper to store logs in S3 or CloudWatch?

For storage alone, S3 is cheaper: $0.023 per GB-month in S3 Standard and $0.004 in Glacier Instant Retrieval, against $0.03 in CloudWatch (us-east-1). But storage is rarely the big line. Ingestion at $0.50 per GB usually is.

Does exporting CloudWatch Logs to S3 reduce costs?

Only the storage part. By the time an export task or subscription filter runs, you’ve already paid CloudWatch ingestion. To cut ingestion, send logs to S3 directly or use the Infrequent Access class.

Can I change an existing log group to the Infrequent Access class?

No. The log class is set when the group is created. Create a new IA group and point the application or service at it.

What is the default retention for CloudWatch Logs?

Never expire. Log groups created by services like Lambda keep everything until you set a retention policy.

Four steps, in order

  1. Run the two read-only queries above: top 20 log groups by ingestion, and every group with no retention.
  2. Agree a retention period with each owner and set it.
  3. Move flow logs, resolver query logs and WAF logs to direct S3 delivery.
  4. For the biggest application groups, split by severity: alerting lines stay in Standard, the rest go to IA or Firehose.

Log groups left behind by deleted functions are the same kind of waste we cover in idle and orphaned AWS resources. New log groups keep appearing with Never expire, so step 1 is worth repeating. Kostrix does it on every scan: it flags groups with no retention, and high-volume Standard groups that use no Standard-only feature and could move to Infrequent Access.