Concurrent message processing middleware for FastStream with aiokafka
Project description
faststream-concurrent-aiokafka
Concurrent message processing middleware for FastStream with aiokafka.
By default FastStream processes Kafka messages sequentially — one message at a time per subscriber. This library turns each incoming message into an asyncio task so multiple messages are handled concurrently, while keeping offset commits correct and shutdown graceful.
Features
- Concurrent message processing via asyncio tasks
- Configurable concurrency limit (semaphore-based)
- Batch offset committing per partition after each task completes
- Rebalance-safe: pending offsets are flushed on partition revocation via
ConsumerRebalanceListener - Graceful shutdown: waits up to
shutdown_timeout_sec(default 20 s) for in-flight tasks before exiting - Signal handling (SIGTERM / SIGINT) triggers graceful shutdown
- Handler exceptions are logged but do not crash the consumer
- Health check helper to probe handler status from a
ContextRepo
📦 PyPi
📝 License
Installation
pip install faststream-concurrent-aiokafka
Quick Start
ack_policy=AckPolicy.MANUAL is required on every concurrent subscriber — the middleware enforces this at runtime.
Without it, FastStream would commit offsets before processing tasks complete, causing silent message loss on crash.
Subscribers that use other ack policies are automatically passed through without concurrent processing.
AsgiFastStreamnote: its lifespan receives an app-levelContextReposeparate frombroker.context. Passbroker.contextexplicitly instead of the injected argument.
from contextlib import asynccontextmanager
from faststream import ContextRepo
from faststream.asgi import AsgiFastStream
from faststream.kafka import KafkaBroker
from faststream.middlewares import AckPolicy
from faststream_concurrent_aiokafka import (
KafkaConcurrentProcessingMiddleware,
initialize_concurrent_processing,
stop_concurrent_processing,
)
broker = KafkaBroker(...)
# Register KCM on the broker before any other middleware (see DI note below)
broker.add_middleware(KafkaConcurrentProcessingMiddleware)
@asynccontextmanager
async def lifespan(_context: ContextRepo):
await initialize_concurrent_processing(
context=broker.context,
concurrency_limit=20, # max concurrent tasks (minimum: 1)
commit_batch_size=100, # commit after this many completed tasks
commit_batch_timeout_sec=5.0, # or after this many seconds
)
try:
yield
finally:
await stop_concurrent_processing(broker.context)
app = AsgiFastStream(broker, lifespan=lifespan)
@broker.subscriber("my-topic", group_id="my-group", ack_policy=AckPolicy.MANUAL)
async def handle(msg: str) -> None:
...
# Subscribers without AckPolicy.MANUAL are passed through unchanged
@broker.subscriber("other-topic", group_id="other-group")
async def handle_other(msg: str) -> None:
...
Core Concepts
KafkaConcurrentProcessingMiddleware
A FastStream BaseMiddleware subclass. Add it to your broker to enable concurrent processing. It wraps each incoming message in an asyncio task submitted to KafkaConcurrentHandler.
KafkaConcurrentHandler
The processing engine. Manages:
- An
asyncio.Semaphoreto enforceconcurrency_limit - In-flight task tracking via a counter +
asyncio.Event(each task's done-callback releases the semaphore, decrements the counter, and sets the event when it reaches zero) - A
KafkaBatchCommitterfor offset commits - Signal handlers for graceful shutdown
- An optional
ConsumerRebalanceListener(viahandler.create_rebalance_listener()) that flushes pending commits when partitions are revoked
KafkaBatchCommitter
Runs as a background asyncio task. A streaming loop absorbs KafkaCommitTask objects into per-partition pending state and commits each partition's contiguous-done prefix when total pending crosses commit_batch_size, when commit_batch_timeout_sec fires, or when commit_all/close sets the flush event. Cancelled tasks are treated as a hard boundary — the offset advance stops at the cancelled task so it gets redelivered on restart (at-least-once). If the committer's task dies, CommitterIsDeadError is raised to callers.
API Reference
initialize_concurrent_processing(context, ...)
Create and start the concurrent processing handler; store it in FastStream's context.
| Parameter | Default | Description |
|---|---|---|
context |
required | FastStream ContextRepo instance |
concurrency_limit |
10 |
Max concurrent asyncio tasks (minimum: 1) |
commit_batch_size |
10 |
Max messages per commit batch |
commit_batch_timeout_sec |
10.0 |
Max seconds before flushing a batch |
shutdown_timeout_sec |
20.0 |
Max seconds to wait for the batch committer to flush AND for in-flight handlers to finish during graceful shutdown |
Returns the KafkaConcurrentHandler instance.
stop_concurrent_processing(context)
Flush pending commits, wait for in-flight tasks (up to shutdown_timeout_sec), then stop the handler.
is_kafka_handler_healthy(context)
Returns True if the KafkaConcurrentHandler stored in context is running and healthy, False otherwise (not initialized, stopped, or observer task dead). Useful for readiness/liveness probes.
KafkaConcurrentProcessingMiddleware
FastStream middleware class. Register it via broker.add_middleware(...). See Quick Start for usage examples.
Must be outermost.
consume_scopefires the handler as a background task and returnsNoneimmediately. Any middleware that wraps it on the outside will see that premature return and misfire — wrong timing, early cleanup, or missed exceptions. Middlewares added after it (i.e. inner in the chain) run correctly inside the background task.
DI framework compatibility (modern-di-faststream and similar)
DI frameworks like modern-di-faststream register a broker-level middleware that creates a REQUEST-scoped dependency container around each message. If that middleware is outer to KafkaConcurrentProcessingMiddleware, its scope closes as soon as consume_scope returns — before the background task runs — so any dependencies resolved inside the task (database sessions, repositories, …) are created from an already-closed container. Their finalizers never run, leaving connections unreturned to the pool.
Fix: call broker.add_middleware(KafkaConcurrentProcessingMiddleware) before setup_di(...) (or any equivalent DI bootstrap call). FastStream stacks broker middlewares so the first registered is outermost; adding KCM first makes it wrap the DI middleware, so the DI middleware runs inside KCM's background task and can manage the scope lifetime correctly.
broker = KafkaBroker(...)
broker.add_middleware(KafkaConcurrentProcessingMiddleware) # registered first → outermost
modern_di_faststream.setup_di(app, container=container) # registered after → inner to KCM
How It Works
-
Message dispatch: On each incoming message,
consume_scopecallshandle_task(), which acquires a semaphore slot then fires the handler coroutine as a backgroundasyncio.Task. -
Concurrency control: The semaphore blocks new tasks when
concurrency_limitis reached. The slot is released via a done-callback when the task finishes or fails. -
Offset committing: Each dispatched task is paired with its Kafka offset and consumer reference and enqueued in
KafkaBatchCommitter. Once the task completes, the committer groups offsets by partition and callsconsumer.commit(partitions_to_offsets)withoffset + 1(Kafka's "next offset to fetch" convention). -
Rebalance handling: When Kafka revokes a partition, the
ConsumerRebalanceListener(returned byhandler.create_rebalance_listener()) callscommitter.commit_all()to flush pending offsets before the partition is reassigned. This prevents in-flight messages from being redelivered to the new owner. -
Graceful shutdown:
stop_concurrent_processingflushes the committer, then awaits all in-flight tasks via anasyncio.Event(set when the in-flight counter reaches zero) bounded byshutdown_timeout_sec(default 20 s), then removes the signal handlers.
Requirements
- Python >= 3.11
faststream[kafka]
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file faststream_concurrent_aiokafka-0.4.0.tar.gz.
File metadata
- Download URL: faststream_concurrent_aiokafka-0.4.0.tar.gz
- Upload date:
- Size: 13.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9124ae793b3051d9378be19dda0b8d2e0d2b646196278a868bc9d68dae50a41
|
|
| MD5 |
5b4f4555c708cf740e4c594077bdc61a
|
|
| BLAKE2b-256 |
82532345b00e5719e5e9b841aa613296cda31815b836061e44bafb7de91b81b2
|
File details
Details for the file faststream_concurrent_aiokafka-0.4.0-py3-none-any.whl.
File metadata
- Download URL: faststream_concurrent_aiokafka-0.4.0-py3-none-any.whl
- Upload date:
- Size: 17.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a3d9bd72ae7b11c1fd6faefd994e60a7249366b19b459d82ea9734ed6c9ed9d
|
|
| MD5 |
7798816eb0f1af218e8946fc298d5a74
|
|
| BLAKE2b-256 |
a93ce81f54804c12038ad9ee87fdf944075ae730820306c0fc4439d478193c39
|