Sources: CD's devops wiki 02-kafka-rabbitmq/01-kafka-overview.md and 02-lab-kafka-with-docker-compose.md, updated to Kafka 4.x (KRaft, no ZooKeeper) and AKHQ, checked against the Apache Kafka docs and AKHQ releases on 2026-10-28. Lab names only.

Friday evening at Contoso's web shop. The inventory service restarts for a patch. For 40 seconds the order service, which calls inventory directly, gets connection errors and throws the orders away. Nobody finds out until Monday, when 212 customers ask where their stuff is. Nothing was hacked. Two services were simply holding hands too tightly.

TL;DR

  • Problem: services that call each other directly fail together. One restart, one slow dependency, one traffic spike, and work is lost.
  • Who it hurts: customers (lost orders), the business (refunds, reputation), developers on call (every outage is a 2 a.m. mystery), and anyone trying to add a new service without touching the old ones.
  • Fix: put a broker in the middle. Producers write events to a topic; consumers read when they are ready. This post runs Apache Kafka 4.1 and the AKHQ web UI in one Docker Compose file.
  • Cost: two containers, about 1 GB of RAM, zero licence. The 2023 way (ZooKeeper + broker + Confluent Control Center) was three containers, several GB, and a commercial UI.
  • Prove it: send three orders, stop the consumer, send three more, start it again. Nothing is lost.

Table of contents

  • 1. The problem
  • 2. Who gets hurt
  • 3. Cost and value
  • 4. Build it: one Compose file
  • 5. Prove it works
  • Gotchas
  • Checklist
  • What changed recently
  • Sources

1. The problem

Direct calls between services are like phoning a colleague every time you need something. If they are at lunch, you wait. If they quit, you are stuck. If 500 people phone at once, nobody gets through.

A message broker is the office mailbox. You drop the letter and walk away. The colleague empties the mailbox when they are back. Letters wait in order. Nobody loses a letter because someone was at lunch.

Kafka is that mailbox, built for millions of letters a day. The words you need (from my notes):

Word Plain meaning
Event One letter: "Order 1042 was placed", with a key, a value and a timestamp
Producer The service that drops the letter (order service)
Topic A named mailbox (orders, payments)
Partition A mailbox split into slots so several people can empty it in parallel
Consumer / consumer group The services that read; a group shares the slots between its copies
Broker The Kafka server that stores and serves it all

Direct calls versus a broker: the restart no longer loses orders

2. Who gets hurt

Who What they lose without a broker
Customers Orders, bookings, messages silently dropped during a restart or spike
The business Refunds, support tickets, "the site is unreliable" reviews
Developers on call A failure in service A shows up as an error in service B, C and D. Root cause takes hours.
Architecture Adding a new consumer (analytics, fraud check) means changing the producer again
Security / audit No durable record of what happened in which order; replay after an incident is impossible

3. Cost and value

What it costs to run this lab (checked 2026-10-28):

Stack Containers RAM on my laptop Licence
2023 lab (my old notes): ZooKeeper + Kafka + Confluent Control Center 3 several GB; Control Center alone is the heavy one Control Center is Confluent's commercial UI (free only for a single-broker dev setup)
This post: Kafka 4.1 (KRaft) + AKHQ 2 about 1 GB Apache 2.0 for both. Free.

Kafka 4.0 (March 2025) removed ZooKeeper for good; the broker now coordinates itself with KRaft. AKHQ is an open-source UI that does what most people used Control Center for: topics, messages, consumer groups, lag.

What it is worth: the inventory restart above costs 212 lost orders. With a broker, the same restart costs nothing; the orders wait 40 seconds in the topic. Even on a laptop, learning this pattern is the difference between "we lost data" and "we were a bit late".

Managed alternatives, when the lab becomes real: every cloud sells Kafka as a service (Azure Event Hubs has a Kafka-compatible endpoint, AWS MSK, Confluent Cloud). They start at a few dollars a day for the smallest tier and remove the operations work. The protocol is the same, so what you learn here carries over.

4. Build it: one Compose file

Folder kafka-lab/, file compose.yaml:

services: kafka: image: apache/kafka:4.1.0 container_name: kafka ports: - "29092:29092" # for clients on your laptop  environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_HOST://:29092 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_NUM_PARTITIONS: 3 KAFKA_LOG_DIRS: /var/lib/kafka/data volumes: - kafka-data:/var/lib/kafka/data akhq: image: tchiotludo/akhq:0.28.0 container_name: akhq depends_on: - kafka ports: - "8080:8080" environment: AKHQ_CONFIGURATION: | akhq: connections: lab-kafka: properties: bootstrap.servers: "kafka:9092" volumes: kafka-data: 

Three things in that file matter more than the rest:

  1. Two listeners. kafka:9092 is what containers (AKHQ) use. localhost:29092 is what your laptop uses. Get the advertised listener wrong and clients connect, then time out on the first message.
  2. Fixed versions. 4.1.0 and 0.28.0, not latest. My notes already said this in 2023; it is still the number one cause of "it worked yesterday".
  3. A named volume. Without kafka-data, every docker compose down deletes every topic. (See my Docker volumes post.)

compose.yaml as a picture: two containers, three ports, one volume

Run it:

docker compose up -d docker compose ps 

Both containers show running. First run pulls about 500 MB. Open http://localhost:8080: AKHQ lists the lab-kafka cluster with one node.

Create the topic:

docker exec kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 \ --create --topic orders --partitions 3 --replication-factor 1 

AKHQ: topic orders with 3 partitions, 0 messages

5. Prove it works

Terminal 1, the consumer (the inventory service):

docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh \ --bootstrap-server localhost:9092 --topic orders --group inventory --from-beginning 

Terminal 2, the producer (the order service):

docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh \ --bootstrap-server localhost:9092 --topic orders --property parse.key=true --property key.separator=: 

Type three orders, one per line: 1042:placed, 1043:placed, 1044:placed. They appear in terminal 1 at once.

Now the Friday-evening test: stop the consumer (Ctrl+C in terminal 1), type three more orders, wait 40 seconds, start the consumer again with the same --group inventory. The three orders arrive, in order. Nothing was lost; the group remembered where it stopped.

Before and after: direct call drops 3 orders during a restart; the topic keeps them

In AKHQ, Consumer Groups → inventory shows lag 3 while the consumer is down and 0 after it catches up. That number is the one you will put on a dashboard later.

Gotchas

  • Advertised listeners. localhost:29092 for the laptop, kafka:9092 inside Compose. Mix them up and you get "connected" followed by timeouts.
  • latest is not a version. Pin both images.
  • No volume, no data. docker compose down without the named volume wipes the topics.
  • Replication factor 1 is a lab setting. Three brokers and min.insync.replicas: 2 for anything real; my Kubernetes notes do that with Strimzi.
  • Same key, same partition. Use the order ID as the key so events for one order stay in order.
  • Port 8080 is popular. If AKHQ does not start, something else owns the port.
  • Low on RAM? Drop AKHQ and use kafka-topics.sh; the broker alone is enough to learn on.

Checklist

  • [ ] compose.yaml with pinned apache/kafka:4.1.0 and tchiotludo/akhq:0.28.0
  • [ ] Two listeners: internal kafka:9092, host localhost:29092
  • [ ] Named volume for /var/lib/kafka/data
  • [ ] docker compose ps: both running
  • [ ] Topic orders, 3 partitions, visible in AKHQ
  • [ ] Produce 3, consume 3
  • [ ] Stop consumer, produce 3, restart consumer: 3 arrive, lag back to 0

What changed recently

Checked on 2026-10-28:

  • My 2023 lab used ZooKeeper and Confluent Control Center. Kafka 4.0 (March 2025) removed ZooKeeper; the apache/kafka image runs KRaft by default. Control Center is replaced by AKHQ here (Apache 2.0, version 0.28.0, August 2026).
  • The current Apache Kafka release is 4.4; 4.1.0 is used because it matches my Kubernetes notes. Either works with this file.
  • The official apache/kafka image documents the KAFKA_* environment variables used above; the extra PLAINTEXT_HOST listener and the named volume are my additions.

Sources

Cheat sheet: Kafka + AKHQ in one file, the three settings that matter, the restart test

Thanks for reading! I'm Cuong, and on CD Security Lab I rebuild real Microsoft security cases in a safe lab and explain them simply. Follow along:

Got a question about your own tenant? Drop it in the comments.