/툴 리뷰/Loki vs ELK vs CloudWatch Logs Cost Comparison (Formulas Based on 100 GB/Month)
Tool ReviewsLokiELK

Loki vs ELK vs CloudWatch Logs Cost Comparison (Formulas Based on 100 GB/Month)

We compare Grafana Loki, Elasticsearch (OpenSearch), and CloudWatch Logs using the same formulas for ingest, storage, query, and labor costs across 20 GB, 100 GB, and 500 GB monthly scenarios. Search-performance differences, migration failu

Loki vs ELK vs CloudWatch Logs Cost Comparison (Formulas Based on 100 GB/Month)

When logs cost more than the servers

In practice, the patterns that make log costs spike are almost always the same.

  1. Debug logs left on — A single service bumped to DEBUG during deploy troubleshooting and never reverted ends up accounting for half of total ingest.
  2. Default retention — If you never set a retention period on a CloudWatch Logs log group, the default is "Never expire." Logs from three years ago may still be on the bill.
  3. Scan-based billing — CloudWatch Logs Insights charges for the volume of data scanned by a query. A single auto-refreshing dashboard can quietly inflate the invoice.
  4. Index overhead — With Elasticsearch, the index is often larger than the original logs (it varies a lot with field count and mapping design).

This post is not a "Loki is the best" opinion piece. The goal is to give you formulas that produce an answer once you plug in your log volume, and to show you in advance the places people regret after they switch.

⚠️ Pricing notation: This post does not invent a hypothetical rate card. Cloud unit prices differ by region, date, and commitment, and they change often. Instead we treat unit prices as variables (P_*) and publish the formulas in full. Look up the real numbers on the official pages below after selecting the region you actually use (e.g. ap-northeast-2 Seoul), then plug them in.

  • AWS CloudWatch pricing: https://aws.amazon.com/cloudwatch/pricing/
  • Amazon S3 pricing: https://aws.amazon.com/s3/pricing/
  • Amazon EC2 / EBS pricing: https://aws.amazon.com/ec2/pricing/
  • Amazon OpenSearch Service pricing: https://aws.amazon.com/opensearch-service/pricing/
  • Grafana Cloud Logs pricing: https://grafana.com/pricing/

Values that vary by environment—compression ratio, query frequency, hourly labor cost—are all marked (estimate). Do not take them at face value; replace them with your own numbers.


Common cost-breakdown formula

Split all three stacks into the same four line items and you get an apples-to-apples comparison.

TEXT
Monthly total cost = ingest cost + storage cost + query/scan cost + ops labor

Variable definitions

VariableMeaningNotes
G_inMonthly ingest volume (GB, uncompressed original)Measured
DRetention daysPolicy
C_ratioCompression ratio (stored size ÷ original)(estimate) ~0.1–0.2 is common for text logs
I_ratioIndex overhead multiplier(estimate) ES can add roughly 1× depending on mapping
Q_scanMonthly query scan volume (GB)CloudWatch Insights only
H_opsMonthly ops hours(estimate)
WTeam hourly rate (KRW)Annual salary ÷ annual working hours
P_ingestIngest unit price per GBCheck official pricing page
P_storeStorage unit price per GB-monthCheck official pricing page
P_scanQuery scan unit price per GBCheck official pricing page
P_nodeCompute (EC2/OpenSearch node) hourly unit priceCheck official pricing page

Per-stack expansions

① Self-hosted Loki + S3

TEXT
Ingest cost   = 0                      # self-collected; no separate ingest charge
Storage cost  = G_stored × P_store_s3
  where G_stored = (G_in / 30) × D × C_ratio      # daily avg × retention days × compression ratio (estimate)
Query cost    ≈ S3 GET request fees + data transfer              # transfer = 0 inside the same region; request fees are small
Compute       = (EC2 instance count × 730h × P_node) + EBS (WAL/cache)
Labor         = H_ops × W
Total cost    = storage + request fees + compute + labor

② Elasticsearch / OpenSearch (self-hosted or managed)

TEXT
Storage disk  = (G_in / 30) × D × C_ratio × (1 + I_ratio) × (1 + replica count)
   # With 1 replica, ×2. This is where disk doubles.
Storage cost  = storage disk × P_store_ebs        # or managed storage unit price
Compute       = node count × 730h × P_node          # data nodes + (master) + Kibana/dashboards
Ingest pipeline = Fluent Bit/Logstash running cost (near 0 if sidecar; separate instance if Logstash)
Labor         = H_ops × W                        # includes shard/ILM/rollover management

③ CloudWatch Logs

TEXT
Ingest cost  = G_in × P_ingest
Storage cost = (G_in / 30) × D × C_ratio_cw × P_store_cw
   # CloudWatch stores compressed; use the storage unit price from the pricing page as-is
Query cost   = Q_scan × P_scan            # Logs Insights, billed on data scanned
Labor        = approaches 0 (managed)

Plug-in templates by scenario

The tables below are a worksheet you fill in. Put the unit prices you looked up on the official pages into the unit-price cells.

Scenario A — 20 GB/month / 7-day retention (early-stage startup, 3–5 services)

ItemLoki+S3ES/OpenSearchCloudWatch
Stored volume(20/30)×7×0.15 ≈ 0.7 GB (estimate)0.7×(1+1)×2 ≈ 2.8 GB (estimate)0.7 GB (estimate)
Compute1× t-family (single binary)minimum 1 node + Kibananone
Ingest chargenonenone20 GB × P_ingest
Query chargeS3 request fees (small)none (included in compute)Q_scan × P_scan
Labor2h × W (estimate)4h × W (estimate)0

Key to band A: stored volume is around 1 GB, so storage cost differences are effectively meaningless. The contest is "cost of keeping one instance + labor" vs "CloudWatch ingest fees." At ~20 GB, CloudWatch is usually cheaper on total cost. What you save by self-hosting is less than one EC2 instance plus someone's time.

Scenario B — 100 GB/month / 30-day retention (the baseline for this post)

ItemLoki+S3ES/OpenSearchCloudWatch
Stored volume(100/30)×30×0.15 = 15 GB (estimate)15×(1+1)×2 = 60 GB (estimate, 1 replica)15 GB (estimate)
Apply storage unit price15 × P_store_s360 × P_store_ebs15 × P_store_cw
Compute1–2 small-to-medium instances2 data nodes + dashboards0
Ingest charge00100 × P_ingest
Query chargeS3 GET requests0Q_scan × P_scan
Labor (estimate)4h × W8h × W0.5h × W

Key to band B: the gap between S3 unit price × 15 GB and EBS unit price × 60 GB becomes noticeable. Absolute dollars are still dominated by compute, though. CloudWatch's G_in × P_ingest is now 100 GB worth, so this is where a reversal becomes possible because CloudWatch ingest is priced much higher than storage. Look up P_ingest yourself and multiply by 100.

Scenario C — 500 GB/month / 90-day retention (a service with real traffic)

ItemLoki+S3ES/OpenSearchCloudWatch
Stored volume(500/30)×90×0.15 = 225 GB (estimate)225×2×2 = 900 GB (estimate)225 GB (estimate)
Compute3–5 instances with components split out3+ data nodes (shard design required)0
Ingest charge00500 × P_ingestlargest line item
Query chargeS3 requests + cache0spikes if dashboards query repeatedly
Labor (estimate)8–12h × W16–24h × W1h × W

Key to band C: 500 GB × ingest unit price exceeds the cost of several EC2 instances. Self-hosting starts to be economically justifiable in this band. ES, however, comes with 900 GB of disk plus the burden of shard design.

Volumes in the tables above are estimates assuming compression ratio 0.15, index overhead 1×, and 1 replica. Unit prices vary by region, date, and commitment, so re-check them on the official pricing pages.

3×3 summary (total-cost advantage tendency)

20 GB/month / 7 days100 GB/month / 30 days500 GB/month / 90 days
Loki+S3disadvantaged by laborcompetitivetends to be most favorable
ES/OpenSearchoverkilljustified if you need searchmandatory if search is a product feature
CloudWatchtends to be most favorabledecide after checking ingest unit priceingest fees dominate, unfavorable

Search performance: the index structure is where things get slow

Structure summary

What is indexedHow body search worksBilling trigger
Lokilabels onlyfetch chunks and scan (grep)compute/IO load at scan time
Elasticsearchfull inverted index of the bodyinverted-index lookupcompute and disk always on
CloudWatch Insightsmanaged internal indexscan-basedGB scanned = bill

Where queries diverge

  • Narrow by label, then grep the last 15 minutes → Loki is fast enough. All three stacks are practical.
  • Search a specific error-code string across 30 days of everything, with no labels → Loki slows down sharply (it has to read every matching chunk). ES wins easily.
  • Field-based aggregations / top-N / percentile stats → ES advantage. Loki can approximate with unwrap and label_format, but both expressiveness and performance are limited.
  • Cost-aware query habits → CloudWatch forces you to shrink time ranges because "scan volume = bill." That is both a feature and a source of stress.

Same requirement, three query dialects

Requirement: In the last 1 hour of payment-api logs, find HTTP 500s and aggregate counts by endpoint, highest first.

LOGQL
# LogQL (Grafana Loki)
sum by (endpoint) (
  count_over_time(
    {app="payment-api", env="prod"}
      | json
      | status = "500"
      [1h]
  )
)
# Caution: putting endpoint in a label risks a cardinality explosion.
# Aggregating a value extracted at parse time, as above, is safer,
# but you still have to read all matching logs, so it gets slower as the window grows.
JSON
// Elasticsearch Query DSL
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        { "term":  { "service.keyword": "payment-api" } },
        { "term":  { "http.status": 500 } },
        { "range": { "@timestamp": { "gte": "now-1h" } } }
      ]
    }
  },
  "aggs": {
    "by_endpoint": {
      "terms": { "field": "http.endpoint.keyword", "size": 20, "order": { "_count": "desc" } }
    }
  }
}
SQL
-- CloudWatch Logs Insights
fields @timestamp, endpoint, status
| filter status = 500
| stats count(*) as cnt by endpoint
| sort cnt desc
| limit 20
-- Always narrow the time range to 1 hour before you run this.
-- log groups selected × time range = scan volume = bill.

On expressiveness alone, ES is far ahead. Loki is optimized for "quickly scan a narrow window already constrained by labels," and CloudWatch's syntax is concise—but every query is a cost event, which is a different kind of constraint.


The real cost of self-hosting: how many processes have to stay up

Loki (single binary)Loki (distributed mode)ELK/OpenSearch
Required processesLoki 1 + Promtail/Alloydistributor, ingester, querier, query-frontend, compactorES data nodes (2–3), (dedicated master), Kibana/OpenSearch Dashboards
Collect agentsPromtail / Grafana Alloy / OTel CollectorsameFluent Bit / Filebeat / Logstash
Storage backendobject storage such as S3samelocal disk / EBS (snapshots go to S3)
Ongoing ops workretention policy (simple), query tuning, cardinality watch+ per-component scalingILM policy, shard design, rollover, mapping management, rebalancing
Incident-recovery difficultydata lives in object storage → mostly restartmediumwait on shard recovery and rebalancing after a node loss

The core trade-off looks like this.

  • Loki: the storage tier is S3, so disk-pool and shard-management stress is almost gone. You do need someone who can tune queries when they are slow, and a bad label design will cardinality-explode and blow ingester memory.
  • ES: search is powerful, but ILM, shards, and mappings remain standing work. For a 1–3 person team that is never a small cost.
  • CloudWatch: ops hours approach zero. Instead, the bill is the ops cost. The only savings levers here are three: shorten retention, filter at ingest, and reduce query scan volume.

Savings you can apply immediately while keeping CloudWatch:

Bash
# 1) Find log groups with no retention set (never expire)
aws logs describe-log-groups \
  --query 'logGroups[?retentionInDays==`null`].[logGroupName,storedBytes]' \
  --output table --region ap-northeast-2

# Expected result: a table of never-expire groups.
# Empty result means every group already has a retention policy → go to the next step.

# 2) Set 30-day retention in bulk (run after the policy is decided)
aws logs put-retention-policy \
  --log-group-name /aws/lambda/my-func \
  --retention-in-days 30 --region ap-northeast-2

# Expected result: no output (exit code 0).
# If you get AccessDeniedException, add logs:PutRetentionPolicy to IAM.

# 3) Sort by size to find the culprits
aws logs describe-log-groups --region ap-northeast-2 \
  --query 'reverse(sort_by(logGroups,&storedBytes))[:10].[logGroupName,storedBytes]' \
  --output table

Do not forget reducing ingest itself. A one-line drop filter for health-check logs and static-asset access logs at the OpenTelemetry Collector or Fluent Bit stage often beats a stack migration.

YAML
# Fluent Bit: example of dropping health-check logs
[FILTER]
    Name    grep
    Match   kube.*
    Exclude log  (GET /healthz|GET /readyz|kube-probe)

Korea-environment checklist

  • Check regional unit prices: Seoul region (ap-northeast-2) prices can differ from other regions. Always select Seoul on the pricing page before you read a number, and write the check date into your internal docs.
  • Data-transfer fees: sending logs out of region (e.g. to an overseas SaaS) incurs outbound transfer fees. That is a hidden term in the formula.
  • Korean-language log search: to search Korean body text properly in Elasticsearch you need a morphological analyzer such as nori. With the default analyzer, "결제실패" may not match the way you expect. Conversely, Loki is substring-based, so morphology is a non-issue — for Korean log search, that simplicity is paradoxically an advantage.
  • Encoding: confirm application logs are unified on UTF-8. Leftover EUC-KR will break search on every stack.
  • Support options: before you sign, check cloud-vendor support plans, domestic MSPs, and the Korean-language support scope of Grafana Cloud / Elastic Cloud.
  • Domestic retention requirements: industry and contracts may require logs to stay in a Korea region. Before sending to an overseas-region SaaS, you need internal legal/security review and the actual text of the relevant rules (requirements differ by industry; check official sources).

Conclusion: three-axis decision table

Team sizeMonthly log volumeDominant query patternChoice
1–2 people~50 GBmostly checking recent logsKeep CloudWatch + 30-day retention, health-check filtering
1–2 people50–200 GBrecent logs narrowed by labelLoki single binary + S3
2–3 people200 GB+label-based lookup + simple aggregationsLoki distributed mode + S3
any sizeany volumesearch/aggregation is a product featureES/OpenSearch (no other option)
effectively 0 infra owners~200 GBanythingKeep CloudWatch (labor exceeds the savings)
2–3 people500 GB+hot-log lookup plus long-term analysisSplit Loki (hot) + S3/data lake (cold)

Where people regret the switch (honest failure branches)

1) Kibana dashboards do not move over as-is.

Kibana itemLoki portability
Label-based log-volume trendpossible
Error-rate time seriespossible (rate + parsing)
Field-based top-N table (long windows)effectively impossible / very slow
Percentiles and multi-dimensional correlationimpossible
Free-text global searchclose to impossible (scan hell without labels)

2) Cardinality explosion. The moment you put user_id, trace_id, or request_id in a label, Loki falls over. Labels are only for values with a small, finite set of kinds (app, env, namespace, level); everything else stays in the body and is parsed. This is a rule, not a preference.

3) Aggregation-performance expectations. Build a 30-day aggregation dashboard on Loki and you will hit timeouts often. Long-window aggregations need to be extracted as metrics (Prometheus) or precomputed with a Recording rule.

Three action steps

  1. Measure — pull current monthly ingest (G_in), per-log-group stored volume, and monthly query scan volume. The describe-log-groups command above is the starting point.
  2. Plug in — put unit prices from the official pricing pages and your team's hourly labor rate into the formulas in this post. Never set the self-hosting labor line to 0.
  3. Pilot — do not move everything. Migrate one service and run it for 2–4 weeks. Check: response time of your three most-used queries, dashboard portability, and actual weekly ops hours.

FAQ

Q. At 100 GB/month, is moving to Loki always cheaper? A. No. Storage alone favors S3-backed Loki, but add EC2 instance cost and monthly ops hours (estimate 4+ hours) converted to labor and the comparison can reverse. The 100 GB band is "you have to run the numbers." Look up ingest unit price (P_ingest) on the official pricing page, multiply by 100, and set that next to the EC2 + S3 + labor total.

Q. Fastest way to cut cost while keeping CloudWatch? A. Do these three in order. ① Find never-expire log groups and set them to 30–90 days, ② exclude health-check and static-asset logs at the collect agent, ③ minimize Logs Insights time ranges and log-group selection. ② in particular cuts ingest, storage, and scan fees at once, so it usually has the largest effect.

Q. Isn't running Loki and Elasticsearch together a waste? A. Once you are large enough, it is actually a standard setup. Query the last 7–14 days of hot logs cheaply on Loki, and send only selected logs that need long-term analysis and aggregation (audit logs, payment logs, etc.) to ES or a data lake. The point is not dual-writing everything; it is splitting the stream by purpose, and this compromise also has the lowest migration-failure risk.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.