aiokafka is the asyncio Kafka client for Python, maintained by the aio-libs community. It implements the Kafka protocol in Python, with Cython extensions for encoding and decoding records, instead of wrapping librdkafka. Versions 0.13.0 and 0.14.0 shipped in January and April 2026, 0.15.0rc1 followed on 17 October 2026. pypistats.org counted about 13 million downloads in the 30 days to 23 October 2026. If you run an asyncio service on it, five things deserve a check: when offsets are committed, what a blocking call does to the consumer group, when a sent record is actually delivered, how durable the producer defaults are, and how the group rebalances.
TL;DR: aiokafka gives asyncio services a producer and a consumer with async start and stop, transactions, idempotent production, and the same key-to-partition mapping as the Java client. By default, it commits offsets every 5 seconds, including records your code has not finished, so a crash can skip a record. A blocking call longer than the 10-second session timeout gets the consumer removed from its group, and another member can process the same record again. await producer.send() only queues a record, and a producer that is never stopped drops what is still queued. The producer defaults to acks=1 without idempotence. Rebalances are eager only, and the default assignor cannot share a group with Java consumers on their default settings.
For how aiokafka compares with confluent-kafka and kafka-python on API model, packaging, and Schema Registry, see Kafka Python Clients: confluent-kafka vs aiokafka.
What still works
-
Asyncio from start to stop. Producers and consumers start and stop with coroutines and work as
async withcontext managers. Consumers supportasync for, and each send returns a future that resolves when the broker acknowledges the record. -
Java-compatible key placement. Keys are hashed with Murmur2, as in the Java client. We produced eight keys, including
customer_42,café, and an empty key, to topics with 3, 6, and 12 partitions, and every placement matched the Java client 4.3.1. Records without a key go to a random partition, chosen per record. -
Transactions and static membership. The producer supports idempotent and transactional sends. Consumers support static membership (
group_instance_id), so a consumer that restarts within the session timeout keeps its partitions without a rebalance, and 0.14.0 added fetching from the closest replica (client_rack) when the brokers are configured for it. - Wheels for common platforms. 0.14.0 ships wheels for CPython 3.10 to 3.14 on glibc Linux (x86_64 and aarch64), macOS, and Windows. There are no musllinux wheels, so on Alpine pip builds it from source, which needs a C compiler.
-
Upgrades in 2026. 0.13.0 negotiates protocol versions with each broker and removed the
api_versionoption, so code that passes it fails with aTypeError. 0.14.0 accepts it again as a no-op with a deprecation warning. 0.15.0rc1 drops Python 3.10. - A small maintainer team. In the 90 days to 12 August 2026, the repository had 3 commits and 3 merged pull requests, and one account merged every pull request over the preceding year. The backlog held 157 open issues with a median age of 3.3 years. These figures come from the GitHub snapshot in the full comparison.
What to watch
We tested the behavior below with aiokafka 0.14.0 on Python 3.14.6, against a local Kafka 4.3.1 broker. The test consumers used new groups with auto_offset_reset="earliest".
Auto-commit can commit records before you process them
By default, the consumer commits offsets in the background every 5 seconds (enable_auto_commit=True, auto_commit_interval_ms=5000). It commits the position after the records it has handed to your code, whether or not your code has finished them. If the process crashes after such a commit, those records are not delivered again.
We read the first of three records, held it for 7 seconds as if processing, and then crashed the process. The group's committed offset was already 1, so on restart the consumer began at the second record and never processed the first. With auto-commit off, the restart began at offset 0 and delivered it again.
Turn off auto-commit and commit after the work is done:
consumer = AIOKafkaConsumer(
"orders",
bootstrap_servers="localhost:9092",
group_id="billing",
enable_auto_commit=False,
)
await consumer.start()
try:
async for msg in consumer:
await handle(msg)
await consumer.commit()
finally:
await consumer.stop()
commit() without arguments commits everything the consumer has returned so far, so call it only after those records are done. Committing after every record adds a round trip per record. With getmany(), commit once per batch instead. After a crash, records that finished but were not yet committed are delivered again, so processing must tolerate duplicates.
A blocking call can get the consumer removed from its group
A consumer stays in its group by sending heartbeats, and aiokafka sends them from a task on the event loop. A synchronous call in your handler, such as time.sleep(), requests, or a blocking database driver, stops that task. If the block lasts longer than the session timeout (session_timeout_ms, 10 seconds by default, 45 seconds in the Java client), the broker removes the consumer and gives its partitions to other members.
We ran two consumers in one group on a two-partition topic. One of them blocked for 15 seconds on its first record. About 11 seconds in, the group rebalanced, and the other consumer took over that partition and processed the same record again, because no offset had been committed. When the first consumer unblocked, its automatic commit failed (the log shows UnknownMemberIdError), and it rejoined the group.
Run blocking work in a thread, so the event loop keeps sending heartbeats. With this change, the same 15-second handler caused no rebalance:
async for msg in consumer:
await asyncio.to_thread(process_order, msg.value)
await send() only queues the record
send() returns as soon as the record is in the producer's buffer, and a background task sends the buffer to Kafka. It returns a future that resolves when the broker acknowledges the record, while send_and_wait() waits for that acknowledgment itself. stop() sends whatever is still buffered before it closes the producer.
We queued 2,000 records with await producer.send() and let the program end without calling stop(). Only 1,345 reached the topic, in each of three runs. The process exited with status 0, and the only sign was an "Unclosed AIOKafkaProducer" message. With async with, which calls stop() on exit, all 2,000 arrived. This version also waits for each delivery result:
async with AIOKafkaProducer(bootstrap_servers="localhost:9092") as producer:
futures = [await producer.send("orders", order.value, key=order.key) for order in orders]
await asyncio.gather(*futures)
In a web app such as FastAPI, start the producer when the app starts and stop it on shutdown, for example in the lifespan handler. A failed delivery shows up only in the record's future, so await the futures as above, or use send_and_wait() where the next step depends on the result.
The producer defaults to acks=1 without idempotence
In 0.14.0, the producer defaults to acks=1: the partition leader acknowledges a record after writing it to its local log, without waiting for followers to copy it. If that leader fails right after, an acknowledged record can be lost. Idempotence is off as well (enable_idempotence=False), so a retried send can write a duplicate. The Java client has defaulted to acks=all with idempotence since Kafka 3.0.
Turning on idempotence also sets acks="all", so the leader waits for the in-sync replicas before it acknowledges. It worked against our Kafka 4.3.1 broker without other changes:
producer = AIOKafkaProducer(
bootstrap_servers="localhost:9092",
enable_idempotence=True,
)
Rebalances are eager, and Java consumers need a matching assignor
A rebalance is how a group redistributes partitions when a member joins or leaves. aiokafka supports only eager rebalancing: every member gives up all of its partitions and stops consuming until the new assignment arrives. In our tests, each rebalance revoked every partition from the other consumer, including the ones it got back. Cooperative rebalancing, which moves only the partitions that change, is an open request (issue #1126), and aiokafka does not support the KIP-848 consumer group protocol. Kafka 4.x brokers still accept the classic protocol that aiokafka uses.
The default assignor also differs from Java. aiokafka uses round-robin, while the Java consumer offers range and cooperative-sticky. With a Java console consumer on its default settings already in the group, the aiokafka consumer failed to start with InconsistentGroupProtocolError. With the range assignor, both shared the group and split the partitions:
from aiokafka.coordinator.assignors.range import RangePartitionAssignor
consumer = AIOKafkaConsumer(
"orders",
bootstrap_servers="localhost:9092",
group_id="billing",
partition_assignment_strategy=(RangePartitionAssignor,),
)
This can matter whenever consumers from another client join the same group, for example Java consumers while you move a service between the two.
If you need what aiokafka lacks
A service that runs well on aiokafka can keep running it. If an asyncio service needs cooperative rebalancing, KIP-848, or Confluent's Schema Registry serializers, confluent-kafka also has an AsyncIO producer and consumer, which Confluent stopped marking as experimental in version 2.13. kafka-python 3.x supports cooperative rebalancing, but its API is synchronous. The full comparison linked above covers what each one handles.
Check what your service actually does
Whether you change the commit path, the producer shutdown, or the assignor, check the records and the group itself. A Kafka GUI such as Kafma shows each record's key, partition, headers, and raw bytes, and a consumer group's committed offsets, lag, and members. That lets you confirm that every queued record arrived, see whether commits run ahead of your logs, and check which members remain after a rebalance. Crash recovery and duplicate handling still need tests in your application.
Written by the team behind Kafma.
Top comments (0)