Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

Client guarantees

pyrula-kafka has no dependency on the workflow or agent packages. Every cell below maps to a test in pyrula-kafka/tests/conformance against the broker named under Tested versions. yes means a conformance contract asserts the guarantee holds; partial means it holds within the stated boundary; n/a means the capability has no meaning for that surface.

The compat.confluent row is the migration shim: the native rows’ guarantees, observed through confluent-kafka’s API.

Surfacedeliveryorderingexitoffset committransactionsbackpressurerebalance
produceryesyesyesn/ayesyesn/a
consumeryesn/ayesyesyesyesyes
compat.confluentpartialyesyesyesyesyesyes
  • delivery: A receipt resolves only after the broker acks the record per acks (default all).
    • A record the broker refuses fails its receipt with a typed error, never silently.
    • Receipts resolve without the caller polling.
  • ordering: Records produced to one partition by one producer land in produce order, with enable_idempotence=True.
  • exit: Closing settles what is in flight.
    • Producer: leaving the context manager (or close()) drains every queued record and waits for the actor to stop. purge() before close is the explicit drop.
    • Consumer: wherever a partition leaves (close(), unsubscribe(), unassign(), a revoke), an offset staged with store_offsets() is committed regardless of enable_auto_commit. The fetch position is committed too only when it is True.
  • offset commit: Nothing is committed that was not polled.
    • Auto-commit is off by default (the compat.confluent shim defaults it on, as confluent-kafka does).
    • commit(batch) commits exactly that batch’s offsets, synchronously, and reports failure.
    • store_offsets stages offsets for the next commit.
  • transactions: begin_transaction / produce_*_txn / send_offsets_to_transaction / commit_transaction land as one unit.
    • abort_transaction discards both output and offsets.
    • A failure from a fenced producer is classified fatal and says so.
    • A read_committed consumer (the default) never sees an aborted record, including one that follows another transaction’s commit marker.
  • backpressure: A full queue refuses new work; it never drops accepted work.
    • A produce that would exceed queue_buffering_max_messages / queue_buffering_max_kbytes is refused whole with QueueFull, before any of its records is accepted.
    • purge() and flush() release capacity.
    • Consumer: pause/resume stop and restart fetch without losing position.
  • rebalance: Callbacks fire in order (on_revoke/on_lost before partitions leave, on_assign after they arrive), with the consumer released.
    • A callback may commit, store_offsets, seek, position, committed, pause/resume and inspect the consumer it was raised from.
    • It may acknowledge the phase with assign/unassign (eager) or incremental_assign/incremental_unassign (cooperative-sticky), naming exactly the partitions it received.
    • subscribe, unsubscribe, close, any poll_*, and commit inside on_lost are refused with RebalanceError; the rebalance still completes.
    • Offsets staged for revoked partitions are committed before they leave, regardless of enable_auto_commit (the same rule as close(), unsubscribe(), unassign() and a replacing subscribe()). The fetch position commits too only when it is True.
    • Under cooperative-sticky assignment, unrevoked partitions keep flowing.
    • A consumer evicted for exceeding max_poll_interval_ms (callback time included) gets MaxPollIntervalExceeded from poll_batch, once.

All surfaces: built in. Tested against confluentinc/cp-kafka 7.7.0 (floor); also proven: apache/kafka:4.0.0 (apache-kafka, cluster, 2026-10-04), apache/kafka:4.3.1 (apache-kafka, cluster, 2026-10-04), docker.redpanda.com/redpandadata/redpanda:v25.1.12 (redpanda, single, 2026-10-04).

  • producer: KafkaProducer and AsyncKafkaProducer share one Rust actor; the async class drives it from the event loop’s executor and inherits every cell here (proven directly: TestProducerConformance parametrises over both)
  • consumer: the async consumer (AsyncKafkaConsumer) is exercised by tests/broker/test_async_lifecycle.py, not by this matrix’s conformance rows
  • compat.confluent: A translation over the native rows, proven through the shim’s own API against confluent-kafka 2.15.1’s signatures. Cells hold within the translated config.
    • enable.auto.commit defaults to True, confluent-kafka’s default (native: False).
    • Under auto-commit, the tick, close() and a revoke commit what poll() handed out, never the buffered batch’s fetch position. enable.auto.offset.store is honoured (default true); see Migrating from confluent-kafka.
    • client.id is ignored on the consumer; the producer maps it to client_id.
    • A full queue raises BufferError, as confluent-shaped code expects.
    • confluent’s Producer has no close(), so the exit cell is flush().
  • producer - offset commit, rebalance: the producer has no offsets and no group; the column belongs to the consumer
  • consumer - ordering: the consumer produces nothing; the column belongs to the producer
  • compat.confluent - delivery: delivery callbacks fire on poll()/flush(), as confluent’s do; the without-polling clause is native-only

These need fault injection, so a broker test proves them rather than a matrix cell.

  • Delivery: a retried batch under enable_idempotence=True does not duplicate; a batch whose fate is unknown at the deadline or when a finite retry budget runs out fails its receipt with a DeliveryTimeout that says so, the producer epoch bumps and later sends continue; a send that partly lands fails its receipt with a ProduceError whose report counts what landed and names the partitions that failed - tests/broker/test_idempotent_producer.py. That count is exact on every path: enable_idempotence=True, acks="all", acks=1, acks=0 and inside a transaction. A produce that fails on some partitions retries those partitions only; records the broker already acknowledged are not sent again - tests/broker/test_produce_partial_failure.py.
  • Ordering: order holds after a retried/duplicate-sequence-number ack, not only across concurrent in-flight batches - tests/broker/test_idempotent_producer.py::test_idempotent_duplicate_sequence_number_acks_cleanly.
  • Exit: the same bound flush() drains under (message_timeout_ms, within which a record is retried up to message_send_max_retries times) bounds the drain, and an undelivered record’s receipt fails (logged at ERROR if nobody holds it) - tests/broker/test_producer_close.py.
  • Transactions: after abort_transaction() the producer is reusable and the next transaction commits cleanly - tests/broker/test_transactions.py::test_abort_then_begin_new_transaction, tests/broker/test_eos_edge_cases.py::test_txn_abort_does_not_wedge_next_transaction.

This table compares pyrula-kafka with confluent-kafka-python 2.15.1 and names the contract that verifies each behavior.

You are used toHereProven by
Records still queued when the producer is destroyed are dropped unless you flush() firstLeaving the context manager drains, under the same retry budget flush() uses; what cannot be delivered fails its receipt or is logged; purge() is the opt-outproducer - exit
Delivery callbacks fire only when you call poll(0) or flush()Receipts resolve on the broker’s ack without polling; the shim keeps confluent’s poll-driven callbacksproducer - delivery
BufferError on a full queue, and it is on you to retryThe same refusal, as QueueFull natively and BufferError through the shim; nothing already accepted is droppedproducer - backpressure
A batch that expires after it was sent (delivery.timeout.ms) fails its callback, librdkafka bumps the producer epoch and carries onThe same KIP-360 behaviour: the unknown-fate batch’s receipt fails with a DeliveryTimeout saying it may have landed, the epoch bumps, later sends continue on the same producer; the error’s report says what landedproducer - delivery
Committing in on_revoke yourself when auto-commit is off, because only auto-commit commits stored offsets on revokeOffsets staged with store_offsets are committed before the partition leaves, whether or not auto-commit is on; callbacks fire in orderconsumer - rebalance
Calling commit(), position() on the consumer from inside a rebalance callbackParity: the callback receives the consumer it was registered on and runs with it released, so commit, position, committed, pause/resume, assignment, seek and the acknowledgement calls (assign/unassign eager, incremental_* cooperative-sticky) all work inside it, through the shim tooconsumer / compat.confluent - rebalance
assign() / unassign() / incremental_assign() / incremental_unassign() / seek(), standalone or inside a callback, with the OFFSET_BEGINNING/OFFSET_END/OFFSET_STORED/OFFSET_INVALID sentinels on the TopicPartitions you hand themThe same five calls, resolving the same sentinels; a seek(), assign() or unassign() also drops any record the shim had already read ahead of the old position, so nothing pre-seek is ever handed out and nothing commits below it, unless you staged a lower offset with store_offsets(), which stays in force across a seek as it does in confluentconsumer / compat.confluent - rebalance
_MAX_POLL_EXCEEDED arriving as an error Message you must check msg.error() forThe same eviction, surfaced as a typed error from poll_batch instead of a message you inspect; through the shim, poll() hands it back as an error Message and consume() raises, with KafkaError._MAX_POLL_EXCEEDED as the wrapped error’s code()consumer / compat.confluent - rebalance
err.retriable() / err.txn_requires_abort() / err.fatal() to decide what to do with a transaction failureThe same three, on the native error and on the shim’s KafkaError: a failed transactional produce, partition registration or send_offsets_to_transaction leaves the transaction abortable, as librdkafka’s does, and commit_transaction then fails with that failure’s code, txn_requires_abort() true and neither fatal nor retriable, until you abort; a fenced producer is fatal. A client-side refusal is not abortable, as in confluentproducer - transactions
A delivery callback’s msg.partition() / msg.offset(), or extra bookkeeping to learn where a record landedThe broker’s real partition and offset - the same values ProduceReport.deliveries[i] names for that recordproducer - delivery
close() commits when enable.auto.commit is on and not otherwiseUnder auto-commit the same; with it off, offsets you staged with store_offsets are still committed at close(), a deliberate deviation in the safe direction - confluent would drop themconsumer - exit

Not yet through the shim, or that behaves differently by default

Section titled “Not yet through the shim, or that behaves differently by default”

These confluent-kafka surfaces are unavailable or have different defaults.

  • group.protocol=consumer (KIP-848).
  • The Protobuf Schema Registry serializer (schema_registry.protobuf); the native ProtobufRegistrySerde exists. SerializingProducer / DeserializingConsumer, the Schema Registry client object and the Avro / JSON Schema serializers are through, with the limits the migration page lists.