Kafka Streaming skill for Codex
No lost messages, no surprise duplicates. Codex designs Kafka topics from the ordering key outward, keeps producers on acks=all with idempotence, and writes consumers that commit only after idempotent processing and route poison messages to a dead-letter topic. It diagnoses lag, rebalance storms and duplicates from consumer group output, explains when exactly-once really applies, and asks before resetting offsets or touching topic configs.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- kafka-streaming Installs to ~/.agents/skills/kafka-streaming/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install kafka-streaming The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project kafka-streaming Installs to .agents/skills/kafka-streaming/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/kafka-streaming.zipunzip -o kafka-streaming.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) kafka-streaming PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $kafka-streaming, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and debugs Apache Kafka topics, producers and consumers - partition count and key choice, replication and min.insync.replicas, idempotent producers, consumer groups, offset commits and rebalances, consumer lag, Schema Registry compatibility for Avro or Protobuf, dead-letter topics, and exactly-once with transactions or Kafka Streams. Use when the user creates a topic, writes a producer or consumer in Python or Java, sees duplicate or lost messages, growing consumer lag, rebalance storms, ordering problems, schema compatibility errors, or asks how to get exactly-once processing.
Use it when
- Creating a Kafka topic or choosing partitions and keys
- Writing or reviewing a producer or consumer
- Messages are duplicated, lost, out of order, or lag keeps growing
- Evolving Avro or Protobuf schemas or adding exactly-once processing
Use something else when
- You are streaming database changes into Kafka: use cdc-debezium
- The broker is RabbitMQ or the need is a simple job queue: use rabbitmq-messaging
- The question is event design across services rather than Kafka mechanics: use event-driven-architecture
How to check the result
Kill a consumer mid-batch during a keyed test load and confirm zero lost and zero duplicate results downstream, with per-partition lag returning to near zero.
The mistake it prevents
Committing offsets before processing finishes (or relying on auto-commit), so a crash silently skips messages.
Done well means: every topic has a written key, partition and retention rationale, producers cannot lose acknowledged writes, consumers commit only after processing and tolerate redelivery, schemas evolve under an enforced compatibility rule, and lag is monitored.
Before changing anything
- Read
AGENTS.mdand the client config in code (search forbootstrap.servers,KafkaProducer,Consumer(,@KafkaListener). Identify the client library: Java client,confluent-kafka(librdkafka) for Python, or a framework (Spring Kafka, Kafka Streams). - Learn the broker version and platform (Apache Kafka 3.x or 4.x, Confluent, MSK, Redpanda).
Kafka 4.0 runs only in KRaft mode (no ZooKeeper) and makes the new consumer rebalance
protocol (
group.protocol=consumer) generally available. - Gather facts for the topic in question. CLI calls need network access to the cluster; in a sandbox, ask the user to run them or approve access:
kafka-topics.sh --bootstrap-server $BS --describe --topic orders
kafka-configs.sh --bootstrap-server $BS --entity-type topics --entity-name orders --describe
kafka-consumer-groups.sh --bootstrap-server $BS --describe --group orders-enricher
Topic design
| Decision | Default | Reasoning |
|---|---|---|
| Key | The entity whose events must stay ordered (order_id, account_id) | Ordering is per partition only |
| Partitions | Enough for peak consumer parallelism with headroom (e.g. 12, 24, 48) | Adding partitions later remaps keys and breaks per-key ordering |
| Replication factor | 3 | Survives one broker loss with min.insync.replicas=2 |
min.insync.replicas | 2 (with RF 3) | With acks=all, writes fail rather than land on one replica |
cleanup.policy | delete for events, compact for latest-state-per-key topics | Compacted topics need non-null keys; null values are tombstones |
retention.ms | Longest replay window you need, plus a margin | Consumers offline longer than retention lose data |
kafka-topics.sh --bootstrap-server $BS --create --topic orders \
--partitions 24 --replication-factor 3 \
--config min.insync.replicas=2 --config retention.ms=604800000
Creating, deleting or reconfiguring topics on a shared cluster needs the user’s confirmation. Never delete a topic or reduce retention without explicit approval.
Producers
Kafka clients since 3.0 default to enable.idempotence=true and acks=all; keep them.
Idempotence removes duplicates from producer retries within a session; it does not dedupe
your application sending the same event twice.
from confluent_kafka import Producer
p = Producer({
"bootstrap.servers": BS,
"enable.idempotence": True,
"acks": "all",
"linger.ms": 10,
"compression.type": "zstd",
"delivery.timeout.ms": 120000,
})
def on_delivery(err, msg):
if err:
log.error("delivery failed key=%s err=%s", msg.key(), err) # alert, do not swallow
p.produce("orders", key=order_id, value=payload, on_delivery=on_delivery)
p.poll(0) # serve delivery callbacks
...
p.flush(30) # before exit; check the returned count of undelivered messages
Key every message whose order matters. Null-key messages are spread across partitions.
Consumers
At-least-once is the practical default: process, then commit. Make processing idempotent (upserts keyed on event ID, dedupe table) so redelivery is harmless.
from confluent_kafka import Consumer, KafkaException
c = Consumer({
"bootstrap.servers": BS,
"group.id": "orders-enricher",
"enable.auto.commit": False,
"auto.offset.reset": "earliest", # only applies when the group has no committed offset
"max.poll.interval.ms": 300000,
"partition.assignment.strategy": "cooperative-sticky",
})
c.subscribe(["orders"])
try:
while running:
msg = c.poll(1.0)
if msg is None:
continue
if msg.error():
raise KafkaException(msg.error())
try:
handle(msg) # idempotent
except PermanentError:
send_to_dlq(msg) # orders.dlq with error headers
c.commit(message=msg, asynchronous=False)
finally:
c.close() # leaves the group cleanly, triggers fast rebalance
| Problem | Cause | Fix |
|---|---|---|
| Duplicates after restarts | Commit happened before processing finished, or processing is not idempotent | Commit after processing; idempotent writes |
| Lost messages | Auto-commit committed before a crash; acks=1 on producer | Manual commit; acks=all |
| Rebalance storms | Processing exceeds max.poll.interval.ms | Smaller batches (max.poll.records in Java), faster handler, or hand work to a bounded pool and pause partitions |
| Lag grows on one partition | Hot key | Better key, or split the hot entity |
| Lag grows everywhere | Consumers too slow or too few | Scale consumers up to the partition count; profile the handler |
| Poison message blocks partition | Retrying forever | Bounded retries, then dead-letter topic |
Schemas
- Register Avro, Protobuf or JSON Schema in a Schema Registry; producers serialize with the registry client so every message carries a schema ID.
- Compatibility
BACKWARD(the usual default) lets new consumers read old data: add fields with defaults, never remove required fields or change types. UseFULLfor topics read by many teams. - Test a schema change against the registry’s compatibility endpoint before deploying producers.
Exactly-once
| Pipeline | Approach |
|---|---|
| Kafka -> Kafka (consume, transform, produce) | Transactions: transactional.id on the producer, send_offsets_to_transaction, consumers of the output use isolation.level=read_committed |
| Kafka Streams app | processing.guarantee=exactly_once_v2 |
| Kafka -> database or external API | Not covered by Kafka transactions: idempotent upsert keyed on event ID, or store offsets in the same DB transaction as the data |
For producer, consumer and transactional config details read references/client-configs.md when tuning throughput, latency, or setting up transactions.
Replaying and resetting offsets
kafka-consumer-groups.sh --bootstrap-server $BS --group orders-enricher --topic orders \
--reset-offsets --to-datetime 2025-09-01T00:00:00.000 --dry-run
The group must have no active members. Show the dry-run output to the user and get explicit
approval before re-running with --execute; a reset reprocesses (or skips) real data.
Verification
- Produce a known set of keyed test messages to a non-production topic and confirm counts, per-key order, and no duplicates downstream after killing a consumer mid-batch.
- Watch lag per partition (
kafka-consumer-groups.sh --describe) during a load test. - Confirm producer delivery callbacks report zero errors and
flush()returns 0.
Report template
**Topic:** orders (24 partitions, RF 3, min.insync.replicas 2, retention 7d, key order_id)
**Producer:** idempotent, acks=all, zstd; delivery errors alert via callback
**Consumer:** group orders-enricher, manual commit after upsert, cooperative-sticky, DLQ orders.dlq
**Guarantee:** at-least-once with idempotent upsert on event_id
**Test:** 100k messages, consumer killed twice: 0 lost, 0 duplicate rows; max lag 1,800 What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Kafka Streaming"
short_description: "No lost messages, no surprise duplicates"
default_prompt: "$kafka-streaming Our Kafka consumer processes some orders twice after deploys, find out why" references/client-configs.md
# Kafka client configuration reference
Read this when tuning producer throughput or latency, fixing consumer timeouts, or wiring
transactions. Names are the Java client property names; librdkafka (`confluent-kafka` for
Python, Go, .NET) uses the same names for nearly all of them. Check your client's docs for
defaults, because librdkafka and Java defaults differ in places (for example `linger.ms`).
## Producer
| Property | Recommended | Notes |
|---|---|---|
| `acks` | `all` | Default since Kafka 3.0. Required for idempotence |
| `enable.idempotence` | `true` | Default since 3.0. Needs `acks=all`, retries > 0, in-flight <= 5 |
| `max.in.flight.requests.per.connection` | `5` (max with idempotence) | Ordering is preserved with idempotence up to 5 |
| `delivery.timeout.ms` | 120000 (default) | Upper bound for a send including retries; raise for long broker outages |
| `linger.ms` | 5-50 | Batching delay. Higher means bigger batches, better compression, more latency |
| `batch.size` | 64-256 KB for high throughput | Per-partition batch bytes |
| `compression.type` | `zstd` or `lz4` | zstd compresses better, lz4 is cheaper on CPU |
| `max.request.size` | Keep default unless messages are large | Must align with broker `message.max.bytes` and topic `max.message.bytes` |
Large payloads (over about 1 MB): store the blob in object storage and send a reference.
## Consumer
| Property | Recommended | Notes |
|---|---|---|
| `enable.auto.commit` | `false` for at-least-once | Auto-commit can commit before processing completes |
| `auto.offset.reset` | `earliest` for pipelines, `latest` for live-only views | Only used when the group has no valid committed offset |
| `max.poll.interval.ms` | Longer than worst-case batch processing | Exceeding it removes the consumer from the group |
| `max.poll.records` (Java) | Lower it if batches take too long | librdkafka fetches internally; control batch size in your loop |
| `session.timeout.ms` | 45000 (Java default since 3.0) | Heartbeat-based liveness under the classic protocol |
| `partition.assignment.strategy` | `cooperative-sticky` (librdkafka) / `CooperativeStickyAssignor` (Java) | Incremental rebalances, fewer stop-the-world pauses |
| `group.protocol` | `consumer` on Kafka 4.0+ brokers and clients that support it | New server-side rebalance protocol (KIP-848); assignment strategy is then chosen by the broker |
| `group.instance.id` | Stable per pod | Static membership avoids rebalances on rolling restarts |
| `isolation.level` | `read_committed` when reading transactional output | Hides aborted and open transactions |
| `fetch.min.bytes`, `fetch.max.wait.ms` | Raise for throughput | Trades latency for fewer fetches |
## Transactions (consume-transform-produce)
```python
from confluent_kafka import Consumer, Producer
producer = Producer({"bootstrap.servers": BS, "transactional.id": "enricher-0"})
consumer = Consumer({"bootstrap.servers": BS, "group.id": "enricher",
"enable.auto.commit": False, "isolation.level": "read_committed"})
producer.init_transactions()
consumer.subscribe(["orders"])
while running:
msgs = consumer.consume(num_messages=500, timeout=1.0)
if not msgs:
continue
producer.begin_transaction()
try:
for m in msgs:
producer.produce("orders.enriched", key=m.key(), value=enrich(m.value()))
producer.send_offsets_to_transaction(
consumer.position(consumer.assignment()),
consumer.consumer_group_metadata(),
)
producer.commit_transaction()
except Exception:
producer.abort_transaction()
# rewind to last committed offsets before retrying the batch
raise
```
- `transactional.id` must be stable per logical producer instance (for example per pod ordinal)
so a restarted instance fences its zombie predecessor.
- Transactions cover Kafka writes and offsets only. Side effects to databases or APIs inside
the loop are not rolled back.
- Transaction timeouts (`transaction.timeout.ms`, default 60 s) must exceed batch processing
time and stay below the broker's `transaction.max.timeout.ms`.
## Topic-level settings worth checking
| Config | Meaning |
|---|---|
| `min.insync.replicas` | Minimum replicas that must ack an `acks=all` write |
| `unclean.leader.election.enable` | Keep `false`; `true` can lose committed data |
| `retention.ms` / `retention.bytes` | Time or size limit per partition for `delete` topics |
| `cleanup.policy=compact` plus `min.compaction.lag.ms`, `delete.retention.ms` | Compaction timing and how long tombstones survive |
| `max.message.bytes` | Largest record batch the topic accepts |
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.