Apache Kafka Internals Explained — Partitions, Offsets, and Production Gotchas
Every modern distributed system eventually hits the same wall: services need to talk to each other faster than a synchronous HTTP call allows, and they need to do it reliably even when parts of the system go down. A payment service can't wait for an analytics pipeline to finish before confirming a transaction. An inventory system can't drop an event just because the warehouse app is restarting. The moment you have more than two services exchanging real-time data, you need a message broker — and Kafka has become the industry's default answer.
Kafka solves a deceptively hard problem: durable, ordered, high-throughput, multi-consumer event streaming. Traditional message queues like RabbitMQ delete a message once it's consumed. Kafka keeps it, indexed by offset, for as long as you tell it to. This means you can replay events, onboard new consumers without re-producing data, and audit exactly what happened and when. That's not a queue — it's a distributed, append-only log. Understanding the difference is what separates engineers who use Kafka from engineers who understand Kafka.
By the end of this article you'll be able to reason about partition assignment and why it determines your throughput ceiling, explain exactly how consumer group rebalancing works and when it silently kills your throughput, configure producers for durability versus latency trade-offs, and walk into any production incident involving Kafka and know exactly which knobs to turn first.
The Append-Only Log: Why Kafka's Core Data Structure Changes Everything
Most engineers learn Kafka by learning its API. That's backwards. To really understand it, start with the log — a data structure so simple it's almost boring, yet so powerful it underpins everything from databases (WAL in PostgreSQL) to distributed consensus (Raft).
A Kafka topic is not a queue. It's a named, partitioned, append-only log. When a producer sends a message, Kafka appends it to the end of the log and assigns it a monotonically increasing integer: the offset. Consumers don't pop messages off — they read forward from an offset they track themselves. This is the first mental model shift you need to make.
Because the log is append-only and offset-indexed, reads are sequential I/O — the fastest kind of disk operation. Kafka exploits this aggressively with zero-copy I/O (sendfile syscall), meaning data moves from disk to the network socket without ever touching user-space memory. A single Kafka broker can saturate a 10Gbps NIC handling millions of messages per second, not because Kafka is magic, but because it's built around hardware's natural strengths.
Each partition is an independent log stored as a set of segment files on disk. The active segment is open for writes. Older segments are immutable and eligible for deletion or compaction based on your retention policy. Understanding this file structure is critical when you're debugging disk space alerts or tuning log compaction.
# ── Step 1: Create a topic with 3 partitions and replication factor 3 ── # More partitions = more parallelism, but also more overhead. # Rule of thumb: partitions = (target throughput MB/s) / (single-partition throughput MB/s) kafka-topics.sh \ --bootstrap-server localhost:9092 \ --create \ --topic order-events \ --partitions 3 \ --replication-factor 3 \ --config retention.ms=604800000 # 7 days retention --config segment.bytes=536870912 # Roll a new segment file at 512MB # ── Step 2: Inspect the physical log files on the broker ── # Each partition gets its own directory: <topic>-<partition-number> ls -lh /var/kafka-logs/order-events-0/ # Expected output: # 00000000000000000000.index <- sparse offset index for fast seek # 00000000000000000000.log <- the actual message data, append-only # 00000000000000000000.timeindex <- maps timestamps to offsets # leader-epoch-checkpoint <- tracks leader changes for safe consumer recovery # ── Step 3: Dump raw messages from a partition segment to inspect offsets ── # kafka-dump-log is your best friend for debugging corruption or offset gaps kafka-dump-log.sh \ --files /var/kafka-logs/order-events-0/00000000000000000000.log \ --print-data-log | head -20 # ── Step 4: Check topic configuration and current low/high watermarks ── # High watermark = last offset fully replicated to all ISR members # Log end offset = last offset written (may not be replicated yet) kafka-log-dirs.sh \ --bootstrap-server localhost:9092 \ --topic-list order-events \ --describe | python3 -m json.tool
/var/kafka-logs/order-events-0/:
00000000000000000000.index
00000000000000000000.log
00000000000000000000.timeindex
leader-epoch-checkpoint
baseOffset: 0 lastOffset: 0 count: 1 baseSequence: 0 lastSequence: 0
position: 0 CreateTime: 1718000000000 size: 312 magic: 2
compresscodec: NONE producerId: -1 isTransactional: false
key: orderId-9821 payload: {"status":"PLACED","amount":149.99}
{
"brokers": [
{
"broker": 1,
"logDirs": [
{
"logDir": "/var/kafka-logs",
"partitions": [
{
"partition": "order-events-0",
"size": 1048576,
"offsetLag": 0,
"isFuture": false
}
]
}
]
}
]
}
Partitions, Consumer Groups, and the Parallelism Contract You Must Honour
Here's the rule that most engineers learn the hard way: within a consumer group, each partition is assigned to exactly one consumer at any moment. One consumer can read from multiple partitions, but one partition cannot be read by multiple consumers in the same group simultaneously. This is Kafka's ordering guarantee — and it's also your parallelism ceiling.
If you have a topic with 6 partitions and spin up 8 consumers in the same group, 2 of those consumers will sit completely idle. You've paid for 8 processes and get the throughput of 6. Conversely, if you have 6 partitions and 2 consumers, each consumer reads from 3 partitions — perfectly fine, just not maximally parallel.
Consumer group rebalancing is where production systems silently degrade. A rebalance happens when a consumer joins, leaves, or is considered dead by the group coordinator broker. During a rebalance, all consumption stops — this is the stop-the-world event of the Kafka world. A slow consumer that exceeds max.poll.interval.ms (default 5 minutes) triggers a rebalance even if the consumer process is healthy. This is one of the most misdiagnosed issues in Kafka production systems.
Kafka 2.4+ introduced Incremental Cooperative Rebalancing (the StickyAssignor strategy), which lets consumers release only the partitions they need to give up rather than all of them. This eliminates the full stop-the-world pause for most real rebalances. If you're on an older assignor, you're leaving throughput on the table.
import org.apache.kafka.clients.consumer.*; import org.apache.kafka.common.errors.WakeupException; import org.apache.kafka.common.serialization.StringDeserializer; import java.time.Duration; import java.util.Collections; import java.util.Properties; import java.util.concurrent.atomic.AtomicBoolean; public class OrderEventConsumer { // Shared shutdown flag — set from a shutdown hook so the poll loop exits cleanly private final AtomicBoolean shutdownRequested = new AtomicBoolean(false); public void startConsuming() { Properties consumerConfig = new Properties(); consumerConfig.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker-1:9092,kafka-broker-2:9092"); consumerConfig.put(ConsumerConfig.GROUP_ID_CONFIG, "order-processing-service"); // Consumer group name consumerConfig.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); consumerConfig.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); // CRITICAL: How long between poll() calls before the broker declares this consumer dead. // If your business logic per batch takes > 5 min, raise this — or better, move processing off the poll thread. consumerConfig.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "300000"); // 5 minutes // CRITICAL: How many records to fetch per partition per poll() call. // Lower this if your processing is slow to avoid triggering max.poll.interval.ms consumerConfig.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "100"); // Use CooperativeStickyAssignor to avoid full stop-the-world rebalances (Kafka 2.4+) consumerConfig.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, CooperativeStickyAssignor.class.getName()); // Disable auto-commit — we commit manually AFTER processing succeeds // Auto-commit is at-most-once: it can commit before your DB write finishes consumerConfig.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); // Start reading from the earliest available offset if no committed offset exists consumerConfig.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); try (KafkaConsumer<String, String> orderConsumer = new KafkaConsumer<>(consumerConfig)) { // Register a ConsumerRebalanceListener to log rebalance events in production orderConsumer.subscribe(Collections.singletonList("order-events"), new ConsumerRebalanceListener() { @Override public void onPartitionsRevoked(java.util.Collection<org.apache.kafka.common.TopicPartition> partitions) { // Called BEFORE partitions are taken away — commit offsets here to avoid reprocessing System.out.printf("[REBALANCE] Revoking partitions: %s — committing offsets now%n", partitions); orderConsumer.commitSync(); // Synchronous commit before giving partitions up } @Override public void onPartitionsAssigned(java.util.Collection<org.apache.kafka.common.TopicPartition> partitions) { System.out.printf("[REBALANCE] Assigned partitions: %s%n", partitions); } }); // Register shutdown hook so Ctrl+C triggers a clean wakeup instead of an abrupt kill Runtime.getRuntime().addShutdownHook(new Thread(() -> { shutdownRequested.set(true); orderConsumer.wakeup(); // Causes the blocking poll() to throw WakeupException })); while (!shutdownRequested.get()) { try { // poll() does two things: fetches records AND sends heartbeats to the group coordinator ConsumerRecords<String, String> orderBatch = orderConsumer.poll(Duration.ofMillis(500)); for (ConsumerRecord<String, String> orderRecord : orderBatch) { System.out.printf("Partition=%d | Offset=%d | Key=%s | Value=%s%n", orderRecord.partition(), orderRecord.offset(), // Monotonically increasing per partition orderRecord.key(), orderRecord.value()); processOrder(orderRecord.value()); // Your actual business logic } if (!orderBatch.isEmpty()) { // Synchronous commit: blocks until broker acknowledges. // Guarantees at-least-once delivery — your processing must be idempotent. orderConsumer.commitSync(); System.out.printf("Committed offsets for %d records%n", orderBatch.count()); } } catch (WakeupException shutdownSignal) { // Expected during shutdown — exit the loop cleanly if (!shutdownRequested.get()) throw shutdownSignal; System.out.println("Shutdown signal received — exiting poll loop"); } } } } private void processOrder(String orderJson) { // Simulate order processing — in production this writes to a DB, calls downstream services, etc. System.out.printf("Processing order: %s%n", orderJson); } public static void main(String[] args) { new OrderEventConsumer().startConsuming(); } }
Partition=0 | Offset=0 | Key=orderId-9821 | Value={"status":"PLACED","amount":149.99}
Processing order: {"status":"PLACED","amount":149.99}
Partition=2 | Offset=0 | Key=orderId-9822 | Value={"status":"PLACED","amount":74.50}
Processing order: {"status":"PLACED","amount":74.50}
Committed offsets for 2 records
Partition=0 | Offset=1 | Key=orderId-9823 | Value={"status":"SHIPPED","amount":149.99}
Processing order: {"status":"SHIPPED","amount":149.99}
Committed offsets for 1 records
^C
[REBALANCE] Revoking partitions: [order-events-0, order-events-2] — committing offsets now
Shutdown signal received — exiting poll loop
Producer Durability vs Latency: The acks, retries, and idempotence Triangle
Every Kafka producer configuration is really a negotiation between three forces: how fast you want to send, how sure you want to be the message arrived, and how much you'll tolerate duplicates. Getting this wrong is expensive in production — either you lose data silently or you process orders twice.
The acks setting controls acknowledgement behaviour. acks=0 means fire and forget — fastest, no guarantees. acks=1 means the partition leader acknowledges — fast, but if the leader dies before replication, the message is lost. acks=all (or -1) means every in-sync replica (ISR) must acknowledge — durable, but only as strong as your min.insync.replicas setting.
Here's the trap: acks=all with min.insync.replicas=1 is effectively the same as acks=1, because only one replica needs to be in sync. The correct production configuration is acks=all combined with min.insync.replicas=2 on a cluster with replication factor 3. This tolerates one broker failure with zero data loss.
Idempotent producers (enable.idempotence=true), available since Kafka 0.11, solve duplicate delivery during retries. The broker assigns each producer a PID (Producer ID) and tracks a sequence number per partition. If a retry arrives with the same sequence number, the broker deduplicates it. This gives you exactly-once semantics at the producer level with zero application-side logic. Always enable this in production — the overhead is negligible.
import org.apache.kafka.clients.producer.*; import org.apache.kafka.common.serialization.StringSerializer; import java.util.Properties; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; public class DurableOrderEventProducer { public static void main(String[] args) throws ExecutionException, InterruptedException { Properties producerConfig = new Properties(); producerConfig.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker-1:9092,kafka-broker-2:9092,kafka-broker-3:9092"); producerConfig.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); producerConfig.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // ── DURABILITY CONFIG ────────────────────────────────────────────────── // Require ALL in-sync replicas to acknowledge before the write is confirmed. // Combined with min.insync.replicas=2 on the broker/topic, this means // the write survives a single broker failure. producerConfig.put(ProducerConfig.ACKS_CONFIG, "all"); // Enable idempotent producer: assigns this producer a unique PID. // Broker deduplicates retried messages using PID + partition + sequence number. // REQUIRES: acks=all, retries > 0, max.in.flight.requests.per.connection <= 5 producerConfig.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); // How many times to retry a failed send before giving up. // With idempotence enabled, retries are safe — no duplicates. producerConfig.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE); // Max unacknowledged requests per connection. // Must be <= 5 when idempotence is enabled. // Higher values increase throughput but with idempotence, 5 is the max safe value. producerConfig.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, "5"); // ── THROUGHPUT / LATENCY TUNING ─────────────────────────────────────── // linger.ms: How long to wait for more messages before sending a batch. // 0 = send immediately (low latency, small batches). // 20 = wait 20ms, batch more records, fewer network round-trips (higher throughput). producerConfig.put(ProducerConfig.LINGER_MS_CONFIG, "20"); // batch.size: Max bytes per batch per partition before sending regardless of linger.ms. // Default is 16KB — increasing to 64KB or 128KB dramatically improves throughput // when producing at high volume. producerConfig.put(ProducerConfig.BATCH_SIZE_CONFIG, "65536"); // 64KB // compression.type: Compresses batches before sending. 'snappy' is CPU-light. // 'lz4' is faster. 'zstd' gives best compression ratio (Kafka 2.1+). // Compression happens client-side — reduces network and disk usage. producerConfig.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy"); // buffer.memory: Total memory the producer uses to buffer records before sending. // If the buffer fills up (broker is slow), send() will block for max.block.ms, // then throw a TimeoutException. producerConfig.put(ProducerConfig.BUFFER_MEMORY_CONFIG, "67108864"); // 64MB try (KafkaProducer<String, String> orderProducer = new KafkaProducer<>(producerConfig)) { String orderId = "orderId-9821"; String orderPayload = "{\"status\":\"PLACED\",\"amount\":149.99,\"customerId\":\"cust-4477\"}"; // Using the orderId as the message key ensures all events for the same order // always land on the same partition — preserving per-order event ordering. ProducerRecord<String, String> orderRecord = new ProducerRecord<>( "order-events", // topic orderId, // key — determines partition via murmur2 hash orderPayload // value ); // ── OPTION A: Async send with callback (preferred for high throughput) ── orderProducer.send(orderRecord, (metadata, sendException) -> { if (sendException != null) { // In production: push to a dead-letter topic or alert on-call System.err.printf("FAILED to send order %s: %s%n", orderId, sendException.getMessage()); } else { System.out.printf("[ASYNC] Sent to topic=%s partition=%d offset=%d timestamp=%d%n", metadata.topic(), metadata.partition(), metadata.offset(), metadata.timestamp()); } }); // ── OPTION B: Sync send — blocks until ack or exception ── // Use when you MUST know the write succeeded before proceeding (e.g., financial transactions) try { RecordMetadata syncMetadata = orderProducer.send(orderRecord).get(); // Blocks here System.out.printf("[SYNC] Confirmed at partition=%d offset=%d%n", syncMetadata.partition(), syncMetadata.offset()); } catch (ExecutionException kafkaSendError) { System.err.println("[SYNC] Send failed: " + kafkaSendError.getCause().getMessage()); } // flush() ensures all buffered messages are sent before the try-with-resources closes the producer orderProducer.flush(); } } }
[SYNC] Confirmed at partition=1 offset=48
Replication, ISR, and What Actually Happens When a Broker Dies
Replication in Kafka is leader-follower. Each partition has one leader and N-1 followers spread across brokers. Producers and consumers always talk to the leader — followers exist purely for fault tolerance. The leader tracks which followers are caught up: if a follower hasn't fetched within replica.lag.time.max.ms (default 30 seconds), it's removed from the ISR. When the leader fails, the controller broker (elected via Zookeeper or KRaft) promotes one of the ISR members to leader.
This is where min.insync.replicas (often called MISR) becomes critical. With replication factor 3 and min.insync.replicas=2, you can lose one broker and still accept writes. Lose two brokers and the topic partition becomes unavailable for writes — producers get a NotEnoughReplicasException. This is intentional: Kafka chooses consistency over availability in this scenario, which is the correct trade-off for financial data.
Unclean leader election (unclean.leader.election.enable) is the escape hatch. If all ISR members are dead and this setting is true, Kafka will elect an out-of-sync replica as leader — potentially losing committed messages. This defaults to false in Kafka 0.11+ for exactly this reason. Only enable it for topics where availability beats data integrity.
Kafka 2.8+ introduced KRaft mode, replacing Zookeeper with a Raft-based metadata quorum built into Kafka itself. This eliminates the operational complexity of running a separate Zookeeper ensemble and dramatically speeds up controller failover — from tens of seconds to under a second.
# ── Check ISR status for all partitions of a topic ── # Under-replicated partitions (ISR < replication factor) are a red alert kafka-topics.sh \ --bootstrap-server localhost:9092 \ --describe \ --topic order-events # Expected healthy output: # Topic: order-events Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3 # Topic: order-events Partition: 1 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1 # Topic: order-events Partition: 2 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2 # ── Simulate broker 2 going down, then check ISR shrinkage ── # After replica.lag.time.max.ms (30s), broker 2 is removed from ISR kafka-topics.sh \ --bootstrap-server localhost:9092 \ --describe \ --topic order-events # Output after broker 2 failure: # Topic: order-events Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,3 <-- broker 2 dropped! # Topic: order-events Partition: 1 Leader: 3 Replicas: 2,3,1 Isr: 3,1 <-- leader elected from ISR # Topic: order-events Partition: 2 Leader: 3 Replicas: 3,1,2 Isr: 3,1 # ── Check consumer group lag — the most important production health metric ── # LAG = LOG-END-OFFSET minus CURRENT-OFFSET per partition # Growing lag means consumers can't keep up with producer throughput kafka-consumer-groups.sh \ --bootstrap-server localhost:9092 \ --describe \ --group order-processing-service # Healthy output (lag near zero): # GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID # order-processing-service order-events 0 1047 1047 0 consumer-1-uuid # order-processing-service order-events 1 892 892 0 consumer-2-uuid # order-processing-service order-events 2 1103 1103 0 consumer-1-uuid # ── Set min.insync.replicas at the topic level (overrides broker default) ── # This is the safest place to set it — per topic, not globally kafka-configs.sh \ --bootstrap-server localhost:9092 \ --alter \ --entity-type topics \ --entity-name order-events \ --add-config min.insync.replicas=2 # ── Verify the config was applied ── kafka-configs.sh \ --bootstrap-server localhost:9092 \ --describe \ --entity-type topics \ --entity-name order-events
Topic: order-events Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
Topic: order-events Partition: 1 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1
Topic: order-events Partition: 2 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2
Completed updating config for topic order-events.
Configs for topic 'order-events' are:
min.insync.replicas=2
| Configuration Aspect | High Throughput (Analytics) | High Durability (Financial) |
|---|---|---|
| acks setting | acks=1 (leader only) | acks=all (all ISR members) |
| min.insync.replicas | 1 (effectively same as acks=1) | 2 (tolerates 1 broker loss) |
| enable.idempotence | false (not needed, duplicates OK) | true (exactly-once at producer level) |
| linger.ms | 50-100ms (large batches) | 0-5ms (low latency commits) |
| batch.size | 128KB-512KB (maximize batching) | 16KB-64KB (flush quickly) |
| compression.type | lz4 or zstd (heavy compression) | snappy or none (CPU budget for other work) |
| Consumer: enable.auto.commit | true (at-most-once acceptable) | false (manual commitSync after DB write) |
| Consumer: max.poll.records | 1000+ (process fast in bulk) | 50-100 (safer for slow processing) |
| Replication factor | 2 (cost savings) | 3+ (survive 1 broker loss with ISR=2) |
| unclean.leader.election | true (availability > consistency) | false (never lose committed data) |
🎯 Key Takeaways
- Kafka's performance advantage is architectural, not magic — sequential append-only disk writes plus zero-copy I/O (sendfile syscall) let a single broker saturate a 10Gbps NIC; fight this architecture and you will lose throughput fast
- Partition count is your permanent parallelism ceiling — you can never have more active consumers than partitions in a group, and you cannot reduce partition count without creating a new topic and migrating data, so over-provision by at least 3x your current consumer count
- acks=all is necessary but not sufficient for durability — you must also set min.insync.replicas=2 (with replication factor 3) at the topic level; without MISR=2, acks=all with only one in-sync replica is functionally identical to acks=1
- Consumer group rebalances stop all consumption — switch to CooperativeStickyAssignor (Kafka 2.4+) to eliminate the stop-the-world pause, and always commit offsets in the onPartitionsRevoked callback to avoid reprocessing an entire batch after a rebalance
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Using auto-commit with slow business logic — Symptom: messages appear processed but are actually lost after a consumer crash mid-batch; your DB write succeeded but Kafka re-delivers the same messages to the next consumer instance causing duplicate inserts — Fix: set enable.auto.commit=false and call consumer.commitSync() explicitly only after your DB transaction commits; make downstream operations idempotent using a message-ID deduplication table as a safety net
- ✕Mistake 2: Setting partition count higher than consumer count without planning for future scale — Symptom: idle consumers sitting at 0% CPU while some partitions become hot and lag grows; or conversely, adding consumers later does nothing because partitions are already at 1:1 ratio — Fix: choose partition count based on your 12-month throughput target, not today's consumer count; a safe formula is max(target_throughput_MB / single_consumer_throughput_MB, target_consumer_count * 3) rounded up to a power of 2; you can increase partitions later but you cannot decrease them without creating a new topic
- ✕Mistake 3: Ignoring max.poll.interval.ms when processing is slow — Symptom: consumer group enters a continuous rebalance storm; kafka-consumer-groups.sh shows STATE=PreparingRebalance repeatedly; lag oscillates rather than growing linearly — Fix: either increase max.poll.interval.ms to exceed your worst-case processing time per batch, OR reduce max.poll.records so each poll() returns fewer records that can be processed within the window; better still, decouple Kafka polling from business logic using an internal queue and a separate processing thread pool, keeping the Kafka poll thread fast
Interview Questions on This Topic
- QExplain exactly what happens — at the broker and consumer level — when a Kafka consumer exceeds max.poll.interval.ms. What does the broker do, what does the consumer see, and how does this affect other consumers in the same group?
- QYou have a topic with 12 partitions, replication factor 3, and min.insync.replicas=2. Your cluster has 4 brokers. Two brokers go down simultaneously. What happens to producer writes and consumer reads? Which partitions become unavailable and why?
- QA colleague suggests setting acks=all to guarantee no data loss. You agree but add that acks=all alone is not sufficient. What additional configuration is required, and what specific failure scenario does acks=all without that config fail to protect against?
Frequently Asked Questions
How many Kafka partitions should I create for a new topic?
A practical starting point is to divide your target throughput (MB/s) by the throughput a single consumer can handle (MB/s), then multiply by 3 to give yourself room to scale. Never go below your expected peak consumer count. Remember: you can add partitions later, but you cannot remove them, so err toward more partitions — the overhead of extra partitions is low until you get into the thousands.
What is the difference between Kafka offset and a message ID?
An offset is a monotonically increasing integer that Kafka assigns per partition — it's a position in the log, not a globally unique message identifier. The same offset number (e.g., offset 42) exists independently in every partition of a topic. If you need a globally unique message ID, you must include it in the message payload itself. Offsets are Kafka-internal bookmarks; they tell a consumer where it is in a partition, not which specific message it has.
Is Kafka a database? Can I query it like one?
Kafka is not a database in the traditional sense — it doesn't support secondary indexes, arbitrary queries, or point lookups by key (without scanning the entire log). It's a distributed, append-only event log optimised for sequential writes and sequential reads. However, Kafka Streams and ksqlDB layer SQL-like query and aggregation capabilities on top of Kafka topics, and log compaction lets Kafka retain only the latest value per key, making it behave like a changelog for a key-value store.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.