Cut the CloudWatch log bill per pattern

Share
A CloudWatch Logs bill is mostly ingestion, charged per gigabyte, so a health check that repeats all day pays that rate all day. Over a week of demo traffic, ingestion was 94% of what one cart-service message cost. This post covers the two cheaper places that message can go: the Infrequent Access class, and an S3 bucket the account owns.

The expensive line on a CloudWatch Logs bill is ingestion, the charge for each GB ingested, with storage after it costing far less. A message printed in a loop, a health check or a retry, pays that full rate every time it repeats, whether or not anyone reads it.

Before CloudWatch receives a line, 10x has already read it. The engine runs as a sidecar to a log forwarder (Fluent Bit, the OpenTelemetry Collector, Vector, and more), computing a stable pattern ID for each line, the same on every node running the same engine, library and configuration.

Recording what each pattern costs to ingest is what makes the next step possible: the expensive patterns no alarm or dashboard reads can go to the Infrequent Access class, or to an S3 bucket the account owns, and a line moved to either stays retrievable. What a forwarder does not front stays where it is, so a managed service or a Lambda function writing straight to CloudWatch keeps paying the standard bill; inside a function, the 10x Lambda extension regulates only the events its OpenTelemetry collector exports.

Reading the pattern ID

Here are two lines from an API gateway.

2026-07-12 14:03:11 INFO api-gateway request complete route=/v1/charge status=200 bytes=1420 duration_ms=183
2026-07-12 14:03:12 INFO api-gateway request complete route=/v1/refund status=200 bytes=644 duration_ms=97

Both come from one logging statement, and only the values differ. Some tokens are the same in every line the statement prints: INFO, request complete, and the field names route, status, bytes. The rest change each time: the route, the status code, the byte count, the timestamp. The engine keeps the fixed tokens, the structure, and drops the changing values, leaving the pattern, whose hash is the pattern ID.

A vocabulary supplies those fixed tokens, compiled ahead of time from application and framework code as the set of strings that code can print. Because repetition counts for nothing in the decision, a statement's ID does not drift between hosts or across weeks. Raw access logs are the exception, since no code stands behind them, and there a changing value can pass for a fixed token, leaving one statement with a few IDs.

CloudWatch's pattern command in Logs Insights clusters the events of a query into shared shapes too, but it clusters per query, over the log groups and time range the query names, with nothing durable behind the clusters and no cost history. Anomaly detection tracks patterns durably per log group, yet exposes no identity to key a cost series or a routing rule to.

Either kind of cluster can change when the lines behind it change, measured against Drain, a common clustering algorithm, in its own post. Nothing in the vocabulary depends on the traffic, so the pattern ID stays still while clusters move. A deploy that adds a field or writes a value outside the library into the text stops a metric filter matching, dropping the metric to zero with no signal, although the pattern ID and its cost history continue. A new ID arrives when the message text is rewritten, or once when the library is rebuilt or the naming rules change.

Each pattern has its own numbers, one measured over a week in a demo environment running the OpenTelemetry demo shop, at CloudWatch list rates for us-east-1: ingestion $0.50 per GB in the Standard class, storage $0.03 per GB per month. The pattern is a high-volume INFO line from the cart service, GetCartAsync_called_with_userId, ID YZplfQ4a1Xw, about 1.5 KB per event.

Measured Per day Per 30 days
Lines 16.2M 486M
Volume 24.2 GB 726 GB
Ingestion $12.10 $363
Storage $0.73 $22

Ingestion is 94% of what this message costs. Because CloudWatch bills storage on compressed bytes, the share is higher still. The traffic is synthetic and the rates are list rates, so the numbers show the accounting, not a saving.

Where a moved pattern goes

Tier down sends a pattern to the Infrequent Access class, CloudWatch's cheaper tier, which ingests at half the Standard rate. Those lines stay queryable from the same Logs Insights console the on-call already uses, the read path the class supports. Because the class is fixed when a log group is created, the engine marks the events to move and the forwarder writes them to a second log group in the Infrequent Access class, so CloudWatch bills each group at its own rate. The discount lands on ingestion, the dominant line for these patterns, although storage and Logs Insights queries cost the same in both classes.

The offload lever routes a pattern's events past CloudWatch entirely, which is why they pay no ingestion. An S3 bucket the account owns takes them instead, as plain JSON lines with a pattern-ID index beside them, open to any client that can read JSON. Asking for a pattern ID pulls back only the matching objects, and nothing is re-ingested to answer. Operators holding a raw line rather than an ID paste the line into the same tool, which finds its pattern first.

Choosing the patterns that move

AWS sells the Infrequent Access class and any forwarder can write to S3, neither of which 10x invented. The open part is choosing which pattern to move, and keeping that choice right as the logs change.

Default behaviour is to move nothing. That changes when an operator sets a monthly budget, which the log10x MCP converts into a byte cap per pattern for each container, plus the lever that container's excess takes, and proposes in a pull request for a person to merge. Containers with no budget stay out of it. Where a budget applies, a pattern that spends past its container's cap within a few-minute window, while holding at least 5% of that container's recent volume by default, has only the excess above the cap moved.

Everything at or under the cap, and every pattern below that share, stays in the Standard class, along with the first events of each pattern in each window and, from severity floors, at least half of a pattern's Error events. A wrong choice loses no data: every moved line is queryable in the Infrequent Access group or retrievable from the bucket by ID.

One exemption keeps alarms and metrics working: a pattern that feeds a metric filter, a subscription filter, or the embedded metrics format is never moved by either lever, because the Infrequent Access class does not compute them and offload removes the events they read. A dependency check reads the alarms, dashboards, and metric filters that reference a pattern, so the exempt list matches what uses it. The engine also reports each pattern's pre-move volume as a metric series, with CloudWatch among the backends, so the number stays on the dashboard.

The caps, levers and exemptions are committed files, the caps and levers keyed by container and the exemptions by pattern ID, each entry with a reason and an expiry, and with the per-pattern history they are the record behind every move. Anyone can read that record by installing the log10x MCP server into an AI client, where it runs against a live demo with no account, and the table above came from there. CloudWatch bills each log group at its class rate whatever lands in it, which puts the work in knowing which pattern belongs in the cheaper group before the events are written.