4.3 – Cleanup Tradeoffs & Guarantees
Cleanup Policy Decision Framework
Choosing between retention (delete) and compaction (compact) depends on your data model and use case.
Decision Tree:
Is your data event-based or state-based?
│
├─ Event-based (immutable facts)
│ → Use retention (cleanup.policy=delete)
│ Examples: Logs, clickstreams, transactions
│
└─ State-based (mutable entities)
→ Use compaction (cleanup.policy=compact)
Examples: User profiles, device state, tables
Retention vs Compaction: Comparison
| Aspect | Retention (delete) |
Compaction (compact) |
|---|---|---|
| Data Model | Events (immutable facts) | State (mutable entities) |
| Deletion Criteria | Time or size | Old values for same key |
| What's Retained | All messages within retention period | Latest value per key |
| Disk Growth | throughput × retention × RF | num_unique_keys × message_size × RF |
| Consumer Behavior | Read all events (stream processing) | Read latest state (table snapshot) |
| Replay Capability | Full history replay (up to retention) | Can't replay intermediate states |
| Delete Semantics | All messages expire after N days | Delete specific key (tombstone) |
| Operational Overhead | Low (simple file deletion) | High (CPU/I/O for compaction) |
Use Case: When to Use Retention
Scenario 1: Immutable Events
Example: User clickstream
{user: "alice", action: "click", page: "/home", timestamp: "2023-01-01"}
{user: "alice", action: "click", page: "/about", timestamp: "2023-01-02"}
{user: "alice", action: "purchase", item: "product-123", timestamp: "2023-01-03"}
Why Retention?
- Each event is a distinct fact (no "updates")
- You need full history for analytics (count clicks per day)
- Events are independent (no relationship between consecutive events)
Config:
cleanup.policy=delete
retention.ms=604800000 # 7 days
Scenario 2: High-Volume Time-Series Data
Example: IoT sensor data
{sensor: "temp-sensor-1", value: 72.5, timestamp: "2023-01-01 10:00:00"}
{sensor: "temp-sensor-1", value: 73.1, timestamp: "2023-01-01 10:00:05"}
{sensor: "temp-sensor-1", value: 72.8, timestamp: "2023-01-01 10:00:10"}
Why Retention?
- High throughput (millions of messages/sec)
- Each reading is a separate measurement (not an update)
- Compaction would be too expensive (CPU overhead)
Config:
cleanup.policy=delete
retention.ms=86400000 # 1 day (shorter retention for high volume)
Scenario 3: Append-Only Logs
Example: Application logs
{level: "INFO", message: "User logged in", timestamp: "2023-01-01 10:00:00"}
{level: "ERROR", message: "Database connection failed", timestamp: "2023-01-01 10:00:05"}
{level: "INFO", message: "Retrying connection", timestamp: "2023-01-01 10:00:10"}
Why Retention?
- Logs are append-only (no updates)
- Need full log history for debugging
- No concept of "latest value" per key
Config:
cleanup.policy=delete
retention.ms=259200000 # 3 days
Use Case: When to Use Compaction
Scenario 1: Database Change Data Capture (CDC)
Example: Syncing a database table to Kafka
{id: 123, name: "Alice", age: 25} (insert)
{id: 123, name: "Alice", age: 26} (update)
{id: 123, name: "Alice", age: 27} (update)
Why Compaction?
- Each message represents current state of row 123
- Only the latest state matters (old states are outdated)
- New consumers can rebuild the table from compacted log
Config:
cleanup.policy=compact
min.compaction.lag.ms=60000 # Wait 1 minute before compacting (allow late updates)
Scenario 2: Kafka Streams State Stores
Example: Word count application
{word: "hello", count: 1}
{word: "hello", count: 2}
{word: "hello", count: 3}
Why Compaction?
- Each message represents current count for word "hello"
- Only the latest count matters
- If app restarts, it restores state from compacted changelog (no need to replay all increments)
Config:
cleanup.policy=compact
Scenario 3: Configuration Store
Example: Application configs
{config: "max-connections", value: "100"}
{config: "timeout", value: "30"}
{config: "max-connections", value: "200"} (update)
Why Compaction?
- Each message represents current config value
- Only the latest config matters
- New instances read latest config from compacted topic
Config:
cleanup.policy=compact
Combined Retention + Compaction (Hybrid Cleanup)
Use Case: Keep latest state, but don't keep data forever.
Config:
cleanup.policy=compact,delete
retention.ms=2592000000 # 30 days
min.compaction.lag.ms=60000 # 1 minute
Behavior:
- Compaction runs continuously (keeps latest value per key)
- Retention deletes segments older than 30 days (even if they contain latest values)
Hybrid Cleanup Example 1: User Sessions
Use Case: Track user sessions (latest state per user), but expire inactive users after 30 days.
Config:
cleanup.policy=compact,delete
retention.ms=2592000000 # 30 days
min.compaction.lag.ms=60000 # 1 minute
Timeline:
Day 0: {userId: 123, sessionId: "abc", lastActive: "2023-01-01"}
Day 5: {userId: 123, sessionId: "def", lastActive: "2023-01-05"} (update)
Day 10: {userId: 456, sessionId: "xyz", lastActive: "2023-01-10"}
After compaction (Day 10):
{userId: 123, sessionId: "def", lastActive: "2023-01-05"} (latest)
{userId: 456, sessionId: "xyz", lastActive: "2023-01-10"} (latest)
Day 31 (retention expires):
{userId: 123, ...} ← Deleted (segment older than 30 days)
{userId: 456, ...} ← Retained (only 21 days old)
Result: User 123's session is deleted after 30 days of inactivity, even though it's the latest value.
Hybrid Cleanup Example 2: Shopping Carts
Use Case: Track shopping carts (latest cart per user), but expire carts after 7 days.
Config:
cleanup.policy=compact,delete
retention.ms=604800000 # 7 days
min.compaction.lag.ms=60000 # 1 minute
Timeline:
Day 0: {cartId: "cart-123", items: ["item1", "item2"]}
Day 2: {cartId: "cart-123", items: ["item1", "item2", "item3"]} (update)
Day 4: {cartId: "cart-456", items: ["item4"]}
After compaction (Day 4):
{cartId: "cart-123", items: ["item1", "item2", "item3"]} (latest)
{cartId: "cart-456", items: ["item4"]} (latest)
Day 8 (retention expires):
{cartId: "cart-123", ...} ← Deleted (segment older than 7 days)
{cartId: "cart-456", ...} ← Retained (only 4 days old)
Result: Cart 123 is deleted after 7 days, even though it's the latest cart state.
Hybrid Cleanup Example 3: Device State with Expiration
Use Case: Track IoT device state (latest state per device), but expire devices that haven't reported in 90 days.
Config:
cleanup.policy=compact,delete
retention.ms=7776000000 # 90 days
min.compaction.lag.ms=300000 # 5 minutes
Timeline:
Day 0: {deviceId: "sensor-123", temp: 22.5, battery: 80}
Day 30: {deviceId: "sensor-123", temp: 23.1, battery: 75} (update)
Day 60: {deviceId: "sensor-456", temp: 21.0, battery: 90}
After compaction (Day 60):
{deviceId: "sensor-123", temp: 23.1, battery: 75} (latest, Day 30)
{deviceId: "sensor-456", temp: 21.0, battery: 90} (latest, Day 60)
Day 91 (retention expires):
{deviceId: "sensor-123", ...} ← Deleted (segment older than 90 days)
{deviceId: "sensor-456", ...} ← Retained (only 31 days old)
Result: Sensor 123's state is deleted after 90 days, even though it's the latest state. This is useful for decommissioned or inactive devices.
Hybrid Cleanup Example 4: User Profiles with GDPR Compliance
Use Case: Track user profiles (latest profile per user), but automatically delete profiles after 2 years of inactivity (GDPR compliance).
Config:
cleanup.policy=compact,delete
retention.ms=63072000000 # 2 years (730 days)
min.compaction.lag.ms=60000 # 1 minute
Timeline:
Day 0: {userId: 123, name: "Alice", email: "alice@example.com"}
Day 100: {userId: 123, name: "Alice Smith", email: "alice@example.com"} (update)
Day 200: {userId: 456, name: "Bob", email: "bob@example.com"}
After compaction (Day 200):
{userId: 123, name: "Alice Smith", email: "alice@example.com"} (latest)
{userId: 456, name: "Bob", email: "bob@example.com"} (latest)
Day 731 (retention expires):
{userId: 123, ...} ← Deleted (segment older than 2 years)
{userId: 456, ...} ← Retained (only 531 days old)
Result: User 123's profile is automatically deleted after 2 years, ensuring GDPR compliance (right to be forgotten).
When to Use Hybrid Cleanup
Use Cases:
- ✅ User sessions (latest state, expire after inactivity)
- ✅ Shopping carts (latest cart, expire after N days)
- ✅ Temporary state (latest value, but not forever)
- ✅ IoT device state (latest state, expire inactive devices)
- ✅ GDPR compliance (latest profile, auto-delete after N years)
Trade-offs:
- ⚠️ Complexity: Must tune both compaction and retention configs
- ⚠️ Unpredictable deletion: Latest values are deleted if segment is old
- ⚠️ Higher overhead: Both compaction and segment deletion run
Storage Impact
Retention: Predictable Disk Usage
Formula:
Disk Usage = throughput × retention × replication_factor
Example:
Throughput: 10 MB/sec
Retention: 7 days = 604,800 seconds
Replication Factor: 3
Disk Usage = 10 MB/sec × 604,800 sec × 3 = 18 TB
Key Point: Disk usage is predictable and linear with throughput.
Compaction: Unbounded Disk Usage (Worst Case)
Formula:
Disk Usage = num_unique_keys × message_size × replication_factor
Example:
Unique Keys: 1 million users
Message Size: 1 KB per user
Replication Factor: 3
Disk Usage = 1M × 1 KB × 3 = 3 GB
Key Point: Disk usage grows with number of unique keys, not throughput.
Worst Case: If keys are never reused (e.g., key=timestamp), compaction degrades to retention (every message is the "latest" for its key).
Consumer Impact
Retention: Consumers Read All Events
Behavior:
- Consumers read all messages within retention period
- Consumers see full history (all inserts, updates, deletes)
Example:
Consumer reads:
{user: 123, name: "Alice"}
{user: 123, name: "Alice Smith"} (sees both insert and update)
Use Case: Stream processing (aggregate events, count clicks, etc.)
Compaction: Consumers Read Latest State
Behavior:
- Consumers read latest value per key (after compaction)
- Consumers miss intermediate states (old values are deleted)
Example:
Consumer reads:
{user: 123, name: "Alice Smith"} (only sees latest state, misses initial "Alice")
Use Case: Table snapshot (rebuild database table, restore Kafka Streams state)
Operational Risks
Retention Risks
1. Consumer Lag Exceeds Retention
Scenario: Consumer lags by 8 days, retention is 7 days.
Result: Consumer gets OffsetOutOfRangeException (messages are deleted before consumer reads them).
Mitigation:
- Set retention ≥ 2x max acceptable lag
- Monitor consumer lag (
records-lag-maxmetric) - Alert if lag > 50% of retention period
2. Disk Full
Scenario: High throughput + long retention → disk runs out of space.
Result: Broker refuses writes (DISK_FULL error).
Mitigation:
- Monitor disk usage (
df -h) - Set disk usage alerts (warn at 70%, critical at 85%)
- Reduce retention or add more brokers
Compaction Risks
1. Compaction Falling Behind
Scenario: High write throughput → dirty head grows faster than compaction can clean.
Result: Disk usage grows unbounded.
Symptoms:
max-dirty-percentmetric > 80%- Disk usage increasing despite compaction
Mitigation:
- Increase
log.cleaner.threads(default: 1, try 2-4) - Reduce write throughput (if possible)
- Add more brokers (distribute partitions)
2. Tombstone Retention Too Short
Scenario: delete.retention.ms=1000 (1 second), consumers lag by 10 seconds.
Result: Consumers miss tombstones (don't see deletes).
Example:
Time 0: {user: 123, name: "Alice"}
Time 1: {user: 123, value: null} (tombstone, consumer hasn't read yet)
Time 2: Tombstone deleted (after 1 second)
Time 10: Consumer reads, sees {user: 123, name: "Alice"} (stale data, missed delete)
Mitigation:
- Set
delete.retention.ms≥ max consumer lag (default: 24 hours is usually safe)
3. Compaction CPU Overhead
Scenario: Compacted topic with high throughput → compaction consumes 50%+ CPU.
Result: Slower broker, higher latencies.
Mitigation:
- Tune
log.cleaner.threads(balance between throughput and CPU) - Use fewer partitions (reduces compaction parallelism)
- Consider retention instead of compaction if events are truly immutable
Guarantees and Edge Cases
Retention Guarantees
| Guarantee | Details |
|---|---|
| At-Least Retention | Messages are retained for at least retention.ms (could be slightly longer if segment hasn't rolled) |
| No Message Reordering | Deletion doesn't change message order (offsets are sequential) |
| Atomic Segment Deletion | Entire segment is deleted (not individual messages) |
Compaction Guarantees
| Guarantee | Details |
|---|---|
| At-Least-One Value | At least one value per key is retained (the latest) |
| Ordering Preserved | The latest value for a key has the highest offset |
| Eventual Compaction | Compaction is asynchronous (old values may remain in dirty head for minutes/hours) |
What Compaction Does NOT Guarantee:
- ❌ Instant compaction (old values remain until compaction runs)
- ❌ Exactly-one value per key (multiple values can coexist in dirty head)
Decision Matrix
Use this matrix to choose the right cleanup policy:
| Question | Answer → Retention | Answer → Compaction |
|---|---|---|
| Data Model | Immutable events | Mutable state |
| Need Full History? | Yes (analytics, replay) | No (only latest state) |
| Disk Growth | Predictable (throughput × retention) | Unpredictable (num unique keys) |
| Delete Semantics | Delete all messages after N days | Delete specific key (tombstone) |
| Operational Overhead | Low (simple file deletion) | High (CPU/I/O for compaction) |
| Consumer Pattern | Stream processing | Table snapshot |
Best Practices
1. Use Retention for Events
Examples: Logs, clickstreams, transactions, metrics
Why: Events are immutable facts. Full history is valuable. Retention is operationally simpler.
2. Use Compaction for State
Examples: User profiles, device state, CDC, Kafka Streams state stores
Why: Only latest state matters. Compaction reduces disk usage and enables table snapshots.
3. Combine Retention + Compaction for Bounded State
Config:
cleanup.policy=compact,delete
retention.ms=2592000000 # 30 days
Use Case: Keep latest state, but delete inactive entities after 30 days (e.g., delete inactive users).
4. Monitor Cleanup Health
Key Metrics:
- Retention: Disk usage, consumer lag
- Compaction:
max-dirty-percent,cleaner-recopy-percent, CPU usage
Alerts:
- Disk usage > 80%
- Consumer lag > 50% of retention
max-dirty-percent> 80% (compaction falling behind)
5. Test Cleanup Policies in Staging
Scenario: You change retention from 7 days → 1 day.
Risk: Consumers that lag > 1 day will break (OffsetOutOfRangeException).
Mitigation: Test cleanup policy changes in staging first. Monitor consumer lag before reducing retention.
Cleanup Policy Tuning Cheat Sheet
Quick Decision Tree
Start Here: What is your use case?
Is each message an independent event?
├─ YES → Use Retention (cleanup.policy=delete)
│ └─ Set retention.ms based on replay needs
│
└─ NO → Does each message represent the latest state for a key?
├─ YES → Use Compaction (cleanup.policy=compact)
│ └─ Should state expire after N days?
│ ├─ YES → Use Hybrid (cleanup.policy=compact,delete)
│ └─ NO → Use Compaction only
│
└─ UNSURE → Default to Retention (safer, simpler)
Retention (Delete) Tuning
| Config | Default | Recommendation | Reason |
|---|---|---|---|
retention.ms |
604800000 (7 days) |
Set based on replay needs | Balance storage vs replay window |
retention.bytes |
-1 (unlimited) |
Set if disk is limited | Hard cap on disk usage per partition |
segment.bytes |
1073741824 (1 GB) |
536870912 (512 MB) for fine-grained retention |
Smaller segments = more precise retention |
segment.ms |
604800000 (7 days) |
86400000 (1 day) for time-sensitive data |
Force segment roll for time-based retention |
Example Config (7-day retention, fine-grained):
cleanup.policy=delete
retention.ms=604800000 # 7 days
segment.bytes=536870912 # 512 MB
segment.ms=86400000 # 1 day
Compaction Tuning
| Config | Default | Recommendation | Reason |
|---|---|---|---|
min.compaction.lag.ms |
0 |
60000 (1 minute) |
Allow late-arriving updates before compaction |
max.compaction.lag.ms |
9223372036854775807 (infinite) |
86400000 (1 day) |
Force compaction at least once per day |
min.cleanable.dirty.ratio |
0.5 (50%) |
0.5 (default is good) |
Balance compaction frequency vs overhead |
delete.retention.ms |
86400000 (24 hours) |
≥ max consumer lag |
Ensure consumers see tombstones |
segment.bytes |
1073741824 (1 GB) |
268435456 (256 MB) for high-churn keys |
Smaller segments = faster compaction |
Example Config (compaction with safe tombstone retention):
cleanup.policy=compact
min.compaction.lag.ms=60000 # 1 minute
max.compaction.lag.ms=86400000 # 1 day
min.cleanable.dirty.ratio=0.5 # 50%
delete.retention.ms=86400000 # 24 hours
segment.bytes=268435456 # 256 MB
Hybrid (Compact + Delete) Tuning
| Config | Default | Recommendation | Reason |
|---|---|---|---|
retention.ms |
604800000 (7 days) |
Set based on expiration needs | How long to keep latest state |
min.compaction.lag.ms |
0 |
60000 (1 minute) |
Allow late-arriving updates |
max.compaction.lag.ms |
9223372036854775807 (infinite) |
≤ retention.ms / 2 |
Compact before retention expires |
delete.retention.ms |
86400000 (24 hours) |
≥ max consumer lag |
Ensure consumers see tombstones |
segment.bytes |
1073741824 (1 GB) |
268435456 (256 MB) |
Smaller segments = faster compaction + deletion |
Example Config (30-day expiration, compacted state):
cleanup.policy=compact,delete
retention.ms=2592000000 # 30 days
min.compaction.lag.ms=60000 # 1 minute
max.compaction.lag.ms=1296000000 # 15 days (half of retention)
delete.retention.ms=86400000 # 24 hours
segment.bytes=268435456 # 256 MB
Broker-Level Compaction Tuning
| Config | Default | Recommendation | Reason |
|---|---|---|---|
log.cleaner.threads |
1 |
2-4 for high-throughput clusters |
Parallel compaction (more CPU, faster compaction) |
log.cleaner.io.max.bytes.per.second |
1.7976931348623157E308 (unlimited) |
104857600 (100 MB/sec) if compaction impacts production |
Throttle compaction I/O |
log.cleaner.dedupe.buffer.size |
134217728 (128 MB) |
268435456 (256 MB) for high-cardinality keys |
Larger buffer = more keys in memory |
Example Config (high-throughput cluster):
log.cleaner.threads=4
log.cleaner.io.max.bytes.per.second=104857600 # 100 MB/sec
log.cleaner.dedupe.buffer.size=268435456 # 256 MB
Common Tuning Scenarios
Scenario 1: High-Throughput Event Log (1000 msg/sec)
Goal: Retain 7 days of events, minimize disk usage.
Config:
cleanup.policy=delete
retention.ms=604800000 # 7 days
segment.bytes=536870912 # 512 MB
segment.ms=86400000 # 1 day (force daily segment roll)
compression.type=lz4 # Reduce disk usage
Scenario 2: Database CDC (Change Data Capture)
Goal: Keep latest state per row, compact frequently.
Config:
cleanup.policy=compact
min.compaction.lag.ms=60000 # 1 minute
max.compaction.lag.ms=3600000 # 1 hour (compact frequently)
min.cleanable.dirty.ratio=0.3 # Compact when 30% dirty (more aggressive)
delete.retention.ms=86400000 # 24 hours
segment.bytes=268435456 # 256 MB
Scenario 3: User Sessions (Expire After 30 Days)
Goal: Keep latest session per user, expire after 30 days of inactivity.
Config:
cleanup.policy=compact,delete
retention.ms=2592000000 # 30 days
min.compaction.lag.ms=60000 # 1 minute
max.compaction.lag.ms=1296000000 # 15 days
delete.retention.ms=86400000 # 24 hours
segment.bytes=268435456 # 256 MB
Scenario 4: Low-Latency Compacted Topic
Goal: Minimize dirty head (fast bootstrap for new consumers).
Config:
cleanup.policy=compact
min.compaction.lag.ms=10000 # 10 seconds (compact quickly)
max.compaction.lag.ms=300000 # 5 minutes (force frequent compaction)
min.cleanable.dirty.ratio=0.2 # Compact when 20% dirty (very aggressive)
delete.retention.ms=86400000 # 24 hours
segment.bytes=134217728 # 128 MB (smaller segments = faster compaction)
log.cleaner.threads=4 # Parallel compaction
Trade-off: High CPU/I/O overhead (compaction runs frequently).
Monitoring Checklist
Retention Topics:
- ✅ Disk usage per partition (
kafka.log:type=Log,name=Size) - ✅ Oldest segment age (should be ≤
retention.ms) - ✅ Consumer lag (should be <
retention.ms)
Compaction Topics:
- ✅
max-dirty-percent(should be < 80%) - ✅ Compaction lag (
kafka.log:type=LogCleanerManager,name=max-dirty-percent) - ✅ Tombstone retention (consumers should see tombstones)
- ✅ CPU usage (compaction threads)
Hybrid Topics:
- ✅ All of the above (retention + compaction metrics)
Key Takeaways
Retention vs Compaction: Use retention for immutable events, compaction for mutable state.
Disk Growth: Retention grows with throughput × retention. Compaction grows with num unique keys.
Consumer Behavior: Retention → consumers read all events. Compaction → consumers read latest state.
Operational Overhead: Retention is simple (file deletion). Compaction is CPU/I/O intensive.
Combined Policy:
cleanup.policy=compact,deletekeeps latest state but deletes old data after N days.Risks: Retention → consumer lag exceeds retention, disk full. Compaction → compaction falling behind, tombstone retention too short.
Monitoring: Track disk usage, consumer lag,
max-dirty-percent, CPU usage.Test in Staging: Always test cleanup policy changes in staging before production.
What's Next?
Next Module: 5.1 – Designing Topics & Partition Strategy
What We Covered:
- ✅ Retention vs compaction decision framework
- ✅ Use cases for retention (events, logs, time-series)
- ✅ Use cases for compaction (state, CDC, configs)
- ✅ Combined retention + compaction
- ✅ Storage impact and disk growth
- ✅ Consumer impact (stream vs snapshot)
- ✅ Operational risks and mitigation
What We Didn't Cover (Explicitly Deferred):
- ❌ Topic design strategy → Module 5.1
- ❌ Performance tuning → M6: Operations
Cleanup Tradeoffs & Guarantees: Complete ✅