Skip to main content
PProfileMatch

Kafka Retention, Ordering and When Not to Use Kafka

System Design & ArchitectureLesson 4 of 46 min read

What you'll learn

  • Configure time- and size-based retention and know what it deletes
  • Explain log compaction and tombstones with a keyed-state example
  • Design keys so related events keep their order
  • Decide when a database, a plain queue or an HTTP call beats Kafka

On this page

  1. Why a ride appears to end before it starts
  2. Ordering rules you can rely on
  3. Retention: how long Kafka keeps data
  4. Log compaction: keep the latest value per key
  5. Designing with Kafka, and when not to
  6. Common mistakes
  7. How this shows up in real projects and interviews

Why a ride appears to end before it starts

A ride-hailing trip goes through REQUESTED, ACCEPTED, STARTED and ENDED. The trip service publishes each state change to a topic with two partitions. The billing consumer sometimes sees ENDED first and charges nothing. The cause fits in a dozen lines of code: compare keying the records by trip id with sending them without a key.

order_sim.py
import zlib
from itertools import cycle

steps = [("ride-41", "REQUESTED"), ("ride-41", "ACCEPTED"),
         ("ride-41", "STARTED"), ("ride-41", "ENDED")]

def produce(keyed):
    parts = [[], []]
    rr = cycle([0, 1])
    for key, state in steps:
        p = zlib.crc32(key.encode()) % 2 if keyed else next(rr)
        parts[p].append(state)
    return parts

def consume_interleaved(parts):
    # two partitions are read at different speeds; partition 1 is drained first
    return parts[1] + parts[0]

for keyed in (True, False):
    parts = produce(keyed)
    print("keyed" if keyed else "no key", parts, "->", consume_interleaved(parts))

Output

keyed [['REQUESTED', 'ACCEPTED', 'STARTED', 'ENDED'], []] -> ['REQUESTED', 'ACCEPTED', 'STARTED', 'ENDED']
no key [['REQUESTED', 'STARTED'], ['ACCEPTED', 'ENDED']] -> ['ACCEPTED', 'ENDED', 'REQUESTED', 'STARTED']

The unkeyed run is a stand-in: current Kafka producers batch unkeyed records to a partition rather than strictly alternating, but the principle holds, since without a key there is no relationship between a record and its partition. Once records of one trip are spread across partitions, a consumer reading partitions at different speeds sees them in a scrambled order. Keyed by trip id, all four land in one partition and arrive in the order written.

Ordering rules you can rely on

  • Order is guaranteed per partition: records are read in the order they were appended.
  • To keep related events in order, give them the same key. Everything about one trip, account or device shares a key.
  • Per-key order from a single producer also needs either idempotence enabled or max.in.flight.requests.per.connection=1; otherwise a retried batch can land behind a later one on older setups.
  • Within a consumer group, one consumer owns a partition, so per-key processing stays sequential. If you hand records to a thread pool inside the consumer, you give that guarantee up unless you shard the pool by key.
  • There is no ordering between topics. If a trip's state changes and its payments are in two topics, do not assume cross-topic order; carry a version or timestamp in the payload.

Retention: how long Kafka keeps data

A topic's default cleanup policy is delete: old data is removed by retention. Two limits apply and whichever is reached first wins. retention.ms is the age limit (the broker default is seven days) and retention.bytes is a size limit per partition (unlimited by default). Deletion works on whole segment files, not on individual records, and the segment currently being written is never deleted. So a record can outlive retention.ms until its segment rolls over, governed by segment.ms and segment.bytes. Do not rely on retention as a precise timer.

Change retention on one topic
bin/kafka-configs.sh --bootstrap-server localhost:9092 --alter --entity-type topics --entity-name ride-events --add-config retention.ms=86400000,segment.bytes=268435456

Output

Completed updating config for topic ride-events.

That sets one day of retention and 256 MiB segments (the output line is typical but its wording can vary by version). Consider the consumer side too: a consumer group that is offline for longer than the retention period resumes to find its offset no longer exists. What happens next is decided by auto.offset.reset: earliest silently reprocesses everything still held, latest silently skips the gap, and none throws an error. Pick deliberately.

Log compaction: keep the latest value per key

Set cleanup.policy=compact and Kafka keeps, at minimum, the latest record for every key instead of deleting by age. It suits topics that represent current state, such as a product-price catalog, where history is noise. A record with a key and a null value is a tombstone: it marks the key for deletion, and the tombstone itself is removed after delete.retention.ms. Compaction runs in the background, so readers may still see older values for a while, and the active segment is not compacted. Offsets of surviving records never change, which is why gaps appear.

compact_sim.py
# (offset, key, value); value None is a tombstone (delete marker)
log = [
    (0, "sku-1", "price=100"),
    (1, "sku-2", "price=250"),
    (2, "sku-1", "price=95"),
    (3, "sku-3", "price=40"),
    (4, "sku-2", None),
    (5, "sku-1", "price=90"),
]

def compact(records):
    latest = {}
    for off, key, val in records:
        latest[key] = off
    # keep only the newest record per key; offsets of survivors never change
    kept = [r for r in records if latest[r[1]] == r[0]]
    return [r for r in kept if r[2] is not None]   # real Kafka drops tombstones later, after delete.retention.ms

def retention(records, keep_last):
    return records[-keep_last:]

print("compacted :", compact(log))
print("time/size retention keeping last 3:", retention(log, 3))

Output

compacted : [(3, 'sku-3', 'price=40'), (5, 'sku-1', 'price=90')]
time/size retention keeping last 3: [(3, 'sku-3', 'price=40'), (4, 'sku-2', None), (5, 'sku-1', 'price=90')]

Designing with Kafka, and when not to

Kafka is excellent when you need a durable, replayable, ordered-per-key stream with many independent readers and high write volume: event feeds between services, change-data capture, clickstreams, audit trails and stream processing. It asks something in return: you operate a cluster (or pay for a managed one), you reason about partitions and consumer groups, and you give up per-message features that simpler queues provide.

SituationBetter fit than KafkaWhy
A user clicks "pay" and needs an immediate answerA direct HTTP or RPC callKafka is asynchronous; request/response over it needs correlation plumbing and adds latency
A few hundred background jobs a day with retries, delays and prioritiesA job queue (a managed queue, or a database-backed one)Per-message acknowledgement, delayed delivery and priorities are not what a partitioned log does
Looking up the current state of one record by idA database or cacheKafka is not indexed for random reads; a compacted topic still needs a consumer to materialise it
Strong multi-record transactions with constraintsA relational databaseKafka transactions cover Kafka writes, not business invariants
One service, one consumer, low volume, small teamAn in-process queue or a tableCluster operations cost more than the problem they solve

A useful test before adopting it: write down who reads this data besides the first consumer, whether they need to replay it, and what ordering they need. If the honest answers are "nobody", "no" and "none", you probably want something simpler. For where messaging sits among caching and storage choices, see Redis for a lighter-weight option and System Design for the surrounding trade-offs.

Common mistakes

  • Treating Kafka as a database of record without a plan. With default retention, data silently ages out after a week. Symptom: a replay for a new service finds only recent events.
  • Compacting a topic that needs history. After compaction, the intermediate states are gone for good. Symptom: an audit request for "what was the price last Tuesday" cannot be answered.
  • Choosing a key with low cardinality or heavy skew. One tenant or a default key dominates a partition. Symptom: a single consumer is pinned while the others idle and lag grows on one partition only.
  • Putting a thread pool in the consumer without key-sharding. Per-key order silently breaks under load while passing every test at low volume.
  • Using Kafka for request/response. Developers build reply topics and correlation ids, then fight timeouts and ordering instead of making an HTTP call.

How this shows up in real projects and interviews

In a design interview, naming Kafka is the easy part. Interviewers look for the follow-ups: what is the key, how many partitions, what happens when a consumer is down for a day, what is the retention, and how would you replay after a bug. Strong candidates also say when they would not use it. See the system design interview guide for how to structure those answers. On the job, the most valuable habit is writing a one-page topic spec: purpose, key, partition count, retention or compaction choice, expected consumers and their lag alerts, so the next engineer does not have to reverse-engineer it.

Try it yourself

Pick a real system you know (a bus-ticketing app, a pharmacy stock tracker, anything). List three data flows in it and for each decide: Kafka, a plain queue, a database table or a direct call. For the Kafka one, write its topic spec: key, rough partition count with your reasoning, retention or compaction, and the first consumer's lag alert.

Show a hint

Ask who else will want to read the data later and whether they will need to replay it; if no one needs replay or fan-out, that flow probably does not need Kafka.

Key takeaways

  • Ordering holds per partition only, so related events must share a key; a thread pool inside a consumer can break it.
  • Retention deletes whole segments by age or size, never exactly on time, and consumers that fall behind retention hit auto.offset.reset behaviour.
  • Log compaction keeps the latest value per key and uses tombstones for deletes; it is for current-state topics, not history.
  • Kafka suits replayable, high-volume, multi-reader event streams, and is a poor fit for request/response, small job queues and random lookups.
  • Write a short topic spec covering key, partitions, retention and consumers before creating any production topic.

Test yourself

A trick from this lesson's ride: Delete forgets by age; compact forgets by key, keeping the newest value per key. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

How long does Kafka keep messages?

By default a topic uses the delete policy with retention.ms set to seven days at the broker level and no size limit, but both are configurable per topic. Deletion happens per log segment, so a record may live a little longer than the limit. Topics with cleanup.policy=compact keep the latest record per key instead.

What is log compaction in Kafka?

Log compaction keeps at least the most recent record for each key and removes older ones in the background, instead of deleting by age. A record with a null value is a tombstone that marks a key for deletion. It is used for topics that hold current state, like configuration or product prices.

When should you not use Kafka?

Avoid it for synchronous request/response, low-volume job queues needing delays or priorities, random lookups by id, and strong multi-record transactions. A direct call, a managed queue, a cache or a relational database is simpler and cheaper to run in those cases. Kafka pays off for replayable, high-volume event streams.