AlgoMaster Logo

Your First Producer and Consumer

Medium Priority12 min readUpdated September 13, 2026
Listen to this chapter
Unlock Audio

Kafka becomes easier to understand when you publish a few records and watch them arrive at a consumer. Seeing each record’s key, value, partition, and offset helps connect the concepts to what actually happens.

In this chapter, we’ll use Kafka’s built-in console tools to publish sample order events, consume them in real time, check how far a consumer has read, and replay stored records. We’ll work with synthetic data and a single-partition topic so the flow is easy to follow.

1. Preparing the Environment

Start with a running container named kafka-local using the apache/kafka:4.3.1 image and its default combined broker and KRaft controller configuration. The broker should be reachable at localhost:9092 inside the container.

This setup uses a single node and plaintext connections. Use it for learning, not production.

Open two Bash-compatible terminals. We’ll call them Terminal A and Terminal B. Run all commands on your host machine. Each docker exec command runs Kafka’s tools inside the container, so you don’t need to install Java on your host.

In Terminal B, check that the broker is ready:

Continue once the command succeeds. If you see a connection error, resolve it before moving on.

Next, create a topic for this walkthrough:

Use a fresh topic so earlier records don’t affect the examples. If orders.placed.first-run already exists, choose another name and use it in every command that follows. Keep other producers from writing to this topic, and make sure its records don’t expire during the walkthrough.

Check the topic’s layout:

The output should show one partition, numbered 0, a replication factor of 1, and an assigned leader. Since the topic has only one partition, every record we publish will go to partition 0.

2. Starting a Live Consumer

In Terminal A, start a consumer before publishing any events:

The consumer will keep running and wait for records. If the topic is empty, you won’t see any output yet. Leave it running while you work in Terminal B.

Here’s what the options do:

  • --group first-orders-reader gives the consumer a group name that Kafka uses to track its progress.
  • --from-beginning starts at the earliest available record if the group has no valid saved offset. If it does, the consumer resumes from that offset.
  • The formatter options show each record’s partition, offset, and key alongside its value, which prints by default. The | separator only changes how the output looks; it does not change the stored record.
  • The auto-commit options let the consumer save its progress as it keeps polling Kafka. The one-second interval makes this easier to observe, but it does not guarantee that the consumer commits each printed record’s offset within one second. We use it for this walkthrough, not as a production recommendation.

The diagram shows how records move through the example. The consumer asks Kafka for records, and Kafka returns them in fetch responses:

These console tools are real Kafka clients. The producer sends records to the broker, and the consumer reads them over Kafka’s network protocol. In this example, the consumer simply prints each record to the terminal.

3. Publishing Keyed Order Events

Each input line contains a key, a pipe character (|), and a JSON value. The key, such as ord-1042, identifies the order. The eventId inside the JSON, such as evt-7001, identifies a specific event for that order.

Run this complete command in Terminal B:

The <<'EOF' block is a here-document. It passes the three event lines to the producer as input. Quoting 'EOF' keeps the shell from expanding variables or running command substitutions in the data. Keep each event on one line and the closing EOF on its own line. That closing marker ends the input; the producer does not send it to Kafka.

The -i flag keeps Docker’s standard input open so the producer can read these lines. We don’t need -t because the input comes from the block above.

With parse.key=true, the console producer splits each line at the first |. Everything before it becomes the key, and everything after it becomes the value. The console tool discards the pipe itself. This parsing happens in the console tool.

We’re using JSON for the values, but the console producer does not check the order schema or interpret the fields. Here, totalMinorUnits is an amount in paise, so 129900 means INR 1,299.00. Kafka stores both the key and value as bytes.

The --sync option makes the producer wait for each send to succeed or fail before sending the next record. This makes the walkthrough easier to follow, though it limits throughput. The acks=all setting asks all current in-sync replicas to acknowledge the write. Our topic has only one replica, so there is no backup copy if the broker loses its data.

The producer may finish without printing a success message. Check for errors, then look at Terminal A for the records. Don’t rerun the command just because it finishes quietly: running it again can add three more records with the same event IDs.

4. Inspecting the Records

If you started with a fresh topic and published the events once, Terminal A should show these three records. You may also see log messages.

Notice a few things:

  • Each key appears separately from its JSON value.
  • All three records are in partition 0.
  • Kafka assigned offsets 0, 1, and 2 as it appended the records. The occurredAt timestamps inside the JSON did not determine these offsets.

The diagram shows the stored records and the position where Kafka would append the next record:

With no further writes, the log end offset is 3. Once a consumer has finished processing the record at offset 2 and all earlier records, it can save 3 as the position to resume from. This points to the next record to read; it does not mean a record already exists at offset 3.

The records appear in this order because one producer sent them sequentially to one partition. This example does not show ordering across partitions. An application that processes records in parallel could also finish its work in a different order.

5. Observing Consumer Progress

Leave the consumer running in Terminal A so it can keep polling Kafka and saving its progress. In Terminal B, inspect the consumer group:

Once the consumer commits its offset, the row for orders.placed.first-run, partition 0, should show:

Scroll
FieldExpected valueMeaning
CURRENT-OFFSET3The group’s saved position to resume from
LOG-END-OFFSET3The position after the last stored record
LAG0The difference between those two positions

Consumer IDs and host details will vary. If the committed offset is missing or below 3, leave the consumer running and check again after it has had time to poll and commit.

Zero lag means the group’s saved progress has caught up with the log. It does not tell you whether a database update, payment, or email has completed.

After you see CURRENT-OFFSET reach 3, press Ctrl-C in Terminal A to stop the consumer. Then run the same consumer command again. As long as the saved offset and topic data still exist, the consumer should wait for new records without printing offsets 0 through 2 again. The saved offset takes precedence over --from-beginning.

Automatic commits make this walkthrough easy to observe. In a real application, you need to decide when processing is complete and when it is safe to save progress. If the application crashes after performing an action but before committing, it may repeat that action after restarting. If it commits too early, it may skip unfinished work.

6. Replaying the Stored Records

To read the original records again without changing the saved progress of first-orders-reader, run a separate consumer in Terminal B:

This consumer reads partition 0 directly, starting at offset 0. It does not use the live consumer’s group identity and has automatic commits disabled, so it leaves first-orders-reader’s saved position unchanged.

You should see the same three records, with their original offsets. The consumer exits after reading the third record. If it waits 30 seconds without receiving another record, it times out. A timeout does not mean the consumer read all three records. Check the output.

This is replay: reading stored records again. No producer republishes the records, and Kafka assigns no new offsets.

Starting another consumer with --group first-orders-reader would behave differently. It would join the existing group and share the partition assignments. Since this topic has only one partition, only one member would read from it once the assignment settles.

Replay only works while the records are still available. If Kafka has removed them through retention, this command cannot bring them back.

Reading an event again can also repeat whatever work your application performs. For example, an application that creates invoices should recognize an event it has already handled and avoid creating a duplicate invoice.

7. Troubleshooting and Finishing

Most issues in this walkthrough come from input formatting, saved consumer progress, or the local Kafka setup.

SymptomWhat to check
Producer reports a missing key separatorInclude a pipe (`\
Terminal waits for more here-document inputEnter the closing EOF on its own line, with no spaces before or after it.
Consumer prints a null keyMake sure you enabled key parsing when you published the records. An orderId field inside the JSON does not set the Kafka key.
Consumer waits even though records existThe group may have already committed past those records. Use the replay command to read them again.
Only one of two consumers in the same group receives dataThis is expected: the topic has one partition, which only one group member reads at a time.
Offsets differ from 0, 1, and 2Check whether you reused the topic, another producer wrote to it, or the publishing command ran more than once.
Replay times out before reading three recordsCheck for publishing errors, the correct topic name, expired records, and broker availability.
A command option is unrecognizedMake sure you’re using the Kafka 4.3.1 tools. These examples use --reader-property, --formatter-property, and --command-property.

If you see the same event ID at different offsets, those are separate records in the topic. Kafka does not automatically remove duplicates based on the JSON eventId. Producer retry protections and application duplicate handling address different causes of repeated records.

When you’re done, press Ctrl-C to stop any running console consumers. The here-document producer and the replay consumer exit on their own. Stopping these tools leaves Kafka running and keeps the topic intact.

You can leave the broker running for more practice or stop it while keeping the container:

Your topic and saved consumer progress remain available in the same container. When you return, you can resume from the saved position or replay earlier records.

Summary

You’ve published records with separate keys and JSON values, inspected their partitions and offsets, and checked a consumer group’s saved progress. You’ve also replayed stored records without changing where that group resumes.

Publishing a record, printing it, and saving consumer progress each confirm a different step. This walkthrough shows how records move through Kafka. A real application also needs to recover from failures and avoid repeating business actions when it reads an event again.