| name | infra-kafka-cost-optimizer |
| description | Kafka cost optimization — tiered storage (S3/GCS remote log offload, local.retention.ms vs retention.ms), topic retention right-sizing (per-topic audit SQL), log compaction economics, partition count right-sizing (over-partitioned cluster detection), consumer group idle cleanup, broker instance right-sizing (disk vs compute), Redpanda vs Confluent Cloud cost comparison, MirrorMaker2 cross-region cost, compression savings calculator |
Kafka Cost Optimizer
When to Use
- Reducing Kafka broker disk costs by offloading cold segments to object storage
- Auditing topic retention and eliminating over-retained data
- Right-sizing broker instances for actual throughput workloads
- Detecting and cleaning up abandoned topics and consumer groups
- Comparing self-managed vs managed Kafka (MSK/Confluent Cloud) economics
Tiered Storage (Cold Data to S3/GCS)
Apache Kafka / Strimzi Native Tiered Storage (Kafka 3.6+)
# Enable tiered storage on broker
remote.log.storage.system.enable=true
remote.log.manager.thread.pool.size=4
# S3-backed remote log (kafka-s3-tiered-storage plugin)
remote.log.storage.manager.class.name=org.apache.kafka.server.log.remote.storage.S3RemoteStorageManager
remote.log.storage.manager.impl.prefix=s3.rlsm.
s3.rlsm.bucket.name=my-kafka-tiered-storage
s3.rlsm.region=us-east-1
s3.rlsm.path.prefix=kafka/
# Keep only 1 day on disk, 30 days total (29 days in S3)
# Per-topic override: set local.retention.ms at topic level
remote.log.metadata.manager.class.name=org.apache.kafka.server.log.remote.metadata.storage.TopicBasedRemoteLogMetadataManager
kafka-topics.sh --bootstrap-server kafka:9092 \
--create \
--topic orders \
--config remote.storage.enable=true \
--config local.retention.ms=86400000 \
--config retention.ms=2592000000
kafka-configs.sh --bootstrap-server kafka:9092 \
--entity-type topics \
--entity-name orders \
--alter \
--add-config 'remote.storage.enable=true,local.retention.ms=86400000'
Strimzi with Tiered Storage
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: production
namespace: kafka
spec:
kafka:
config:
remote.log.storage.system.enable: "true"
Topic Retention Audit
import subprocess
import json
from datetime import datetime, timedelta
def audit_topics(bootstrap_server: str) -> list[dict]:
"""Analyze all topics for cost optimization opportunities."""
topics_raw = subprocess.run(
["kafka-topics.sh", "--bootstrap-server", bootstrap_server,
"--list", "--exclude-internal"],
capture_output=True, text=True
).stdout.strip().split("\n")
results = []
for topic in topics_raw:
if not topic:
continue
config_raw = subprocess.run(
["kafka-configs.sh", "--bootstrap-server", bootstrap_server,
"--entity-type", "topics", "--entity-name", topic, "--describe"],
capture_output=True, text=True
).stdout
retention_ms = extract_config(config_raw, "retention.ms", default=604800000)
retention_bytes = extract_config(config_raw, "retention.bytes", default=-1)
desc_raw = subprocess.run(
["kafka-topics.sh", "--bootstrap-server", bootstrap_server,
"--describe", "--topic", topic],
capture_output=True, text=True
).stdout
partition_count = desc_raw.count("Partition:")
log_dirs_raw = subprocess.run(
["kafka-log-dirs.sh", "--bootstrap-server", bootstrap_server,
"--topic-list", topic, "--describe"],
capture_output=True, text=True
).stdout
results.append({
"topic": topic,
"partitions": partition_count,
"retention_days": round(retention_ms / 86400000, 1),
"retention_bytes_GB": round(retention_bytes / 1e9, 2) if retention_bytes > 0 else -1,
})
return sorted(results, key=lambda x: x["retention_days"], reverse=True)
def extract_config(text: str, key: str, default):
import re
match = re.search(rf"{key}=(\d+)", text)
return int(match.group(1)) if match else default
kafka-topics.sh --bootstrap-server kafka:9092 --list | while read topic; do
retention=$(kafka-configs.sh --bootstrap-server kafka:9092 \
--entity-type topics --entity-name "$topic" --describe 2>/dev/null \
| grep retention.ms | awk -F'=' '{print $2}' | awk '{print $1}')
days=$(( ${retention:-604800000} / 86400000 ))
if [ "$days" -gt 30 ]; then
echo "OVER-RETAINED: $topic retention=${days}d"
fi
done
Compression Savings Calculator
def estimate_compression_savings(
uncompressed_mb_per_sec: float,
compression_type: str = "lz4",
) -> dict:
"""Estimate storage and network savings from compression."""
ratios = {
"none": 1.0,
"gzip": 0.25,
"snappy": 0.45,
"lz4": 0.35,
"zstd": 0.28,
}
ratio = ratios[compression_type]
compressed_mb_per_sec = uncompressed_mb_per_sec * ratio
monthly_uncompressed_TB = uncompressed_mb_per_sec * 3600 * 24 * 30 / 1e6
monthly_compressed_TB = monthly_uncompressed_TB * ratio
ebs_savings_usd = (monthly_uncompressed_TB - monthly_compressed_TB) * 1000 * 0.08
s3_savings_usd = (monthly_uncompressed_TB - monthly_compressed_TB) * 1000 * 0.023
return {
"compression_type": compression_type,
"original_MB_per_sec": uncompressed_mb_per_sec,
"compressed_MB_per_sec": round(compressed_mb_per_sec, 1),
"monthly_disk_savings_TB": round(monthly_uncompressed_TB - monthly_compressed_TB, 2),
"ebs_savings_usd_per_month": round(ebs_savings_usd, 0),
"s3_tiered_savings_usd_per_month": round(s3_savings_usd, 0),
}
result = estimate_compression_savings(200, "lz4")
Over-Partitioned Cluster Detection
kafka-topics.sh --bootstrap-server kafka:9092 --describe | \
grep "Leader:" | awk '{print $6}' | sort | uniq -c | sort -rn
kafka-consumer-groups.sh --bootstrap-server kafka:9092 --all-groups --describe \
| awk 'NR>1 {print $1, $2}' \
| sort -u \
| while read group topic; do
consumers=$(kafka-consumer-groups.sh --bootstrap-server kafka:9092 \
--group "$group" --describe 2>/dev/null | grep -c "STABLE")
partitions=$(kafka-topics.sh --bootstrap-server kafka:9092 \
--describe --topic "$topic" 2>/dev/null | grep -c "Partition:")
if [ "$partitions" -gt "$((consumers * 3))" ]; then
echo "OVER-PARTITIONED: topic=$topic partitions=$partitions consumers=$consumers"
fi
done
kafka-topics.sh --bootstrap-server kafka:9092 \
--alter \
--topic dev-staging-events \
--replication-factor 2
Idle Consumer Group Cleanup
#!/bin/bash
BOOTSTRAP="kafka:9092"
DRY_RUN=${1:-true}
kafka-consumer-groups.sh --bootstrap-server $BOOTSTRAP --list | while read group; do
state=$(kafka-consumer-groups.sh --bootstrap-server $BOOTSTRAP \
--group "$group" --describe 2>/dev/null \
| awk 'NR==2 {print $5}')
if [ "$state" = "Empty" ] || [ -z "$state" ]; then
echo "IDLE group: $group (state=${state:-UNKNOWN})"
if [ "$DRY_RUN" = "false" ]; then
kafka-consumer-groups.sh --bootstrap-server $BOOTSTRAP \
--group "$group" --delete
echo " → deleted"
fi
fi
done
Managed vs Self-Managed Cost Model
COST_MODEL = {
"self_managed": {
"broker_instance": "r6i.2xlarge",
"broker_count": 3,
"instance_usd_hr": 0.504,
"ebs_gp3_1tb_usd_month": 80,
"disk_tb_per_broker": 2,
"ops_hours_per_month": 20,
"ops_rate_usd_hr": 150,
},
"msk_serverless": {
"per_partition_hr_usd": 0.0015,
"per_gb_stored_usd": 0.10,
"per_gb_in_usd": 0.10,
"per_gb_out_usd": 0.05,
},
"confluent_cloud": {
"per_cku_hr_usd": 0.44,
"per_gb_stored_usd": 0.00015,
"min_ckus": 1,
},
}
def monthly_self_managed_cost(model: dict) -> float:
instance = model["broker_count"] * model["instance_usd_hr"] * 730
disk = model["broker_count"] * model["disk_tb_per_broker"] * model["ebs_gp3_1tb_usd_month"]
ops = model["ops_hours_per_month"] * model["ops_rate_usd_hr"]
return instance + disk + ops
Quick Wins Checklist
Retention Optimization:
[ ] Audit topics with retention > 7 days — reduce if no consumer needs it
[ ] Enable tiered storage for topics with retention > 1 day (offload 80%+ to S3)
[ ] Switch time-series topics from delete to compact+delete for state topics
Compression:
[ ] Enable lz4 compression on all high-throughput producers (often 50-70% disk reduction)
[ ] Verify compression.type=producer on brokers (don't recompress)
Partitioning:
[ ] Identify topics with partitions > 10x consumer parallelism — schedule reduction
[ ] Consolidate low-volume topics into fewer partitions
Cleanup:
[ ] Delete Empty/Idle consumer groups (reduce broker metadata load)
[ ] Delete dev/test topics not used in >30 days
[ ] Remove topics with 0 bytes/sec for >2 weeks
Right-sizing:
[ ] Profile actual broker CPU/disk I/O — most clusters are disk-bound not CPU-bound
[ ] Consider io-optimized instances (i3.2xlarge) vs memory-optimized for high-throughput
Anti-Patterns
- Default 7-day retention for all topics — event log topics need 7 days, but state changelog topics only need enough to cover consumer downtime (hours); audit per-topic.
- Tiered storage without monitoring remote fetch latency — consumers reading cold data from S3 incur higher latency; monitor
FetchFromFollower vs FetchFromRemote rates.
- Reducing replication factor to save disk — going from RF=3 to RF=1 saves 66% disk but loses all redundancy; use tiered storage instead.
- Partition inflation for throughput — each partition has memory overhead (~1 MB); 10,000 partitions × 64 brokers = 640 MB just for metadata.
- Ignoring cross-AZ data transfer — Kafka replication across AZs incurs ~$0.01/GB transfer; co-locate producers and consumers in the same AZ when possible.
References
- Kafka tiered storage:
kafka.apache.org/documentation/#tiered_storage
- Confluent tiered storage:
docs.confluent.io/platform/current/kafka/tiered-storage.html
- AWS MSK pricing:
aws.amazon.com/msk/pricing/
- Related skills:
[[infra-kafka-platform-review]], [[infra-streaming-reliability-review]], [[de-cost-optimization]], [[terraform-data]]