MongoDB broker and result backend for TaskIQ.
pip install taskiq-mongodbRequires Python 3.11+ and a MongoDB instance (a standalone mongod is enough — no replica set needed).
# broker.py
import asyncio
import os
from taskiq_mongodb import MongoBroker, MongoResultBackend
MONGO_URI = os.environ.get("TASKIQ_MONGODB_URI", "mongodb://root:password@localhost:27017")
broker = MongoBroker(MONGO_URI, "my_app").with_result_backend(
MongoResultBackend(MONGO_URI, "my_app"),
)
@broker.task
async def add_one(value: int) -> int:
return value + 1
async def main() -> None:
await broker.startup()
task = await add_one.kiq(1)
result = await task.wait_result(timeout=5)
print(result.return_value) # 2
await broker.shutdown()
if __name__ == "__main__":
asyncio.run(main())Run a worker in one process and the script above in another:
taskiq worker broker:broker -w 1
python broker.pyMore runnable examples live in examples/.
MongoResultBackend stores task results (and progress, see below) as documents in a collection, one per task id.
from taskiq_mongodb import MongoResultBackend
result_backend = MongoResultBackend(
"mongodb://root:password@localhost:27017", # pragma: allowlist secret
"my_app",
collection_name="task_results", # default
keep_results=True, # keep the document after get_result reads it
ttl_seconds=3600, # auto-expire results after an hour; 0 disables expiry
)It works standalone too, without a broker — useful when you just need durable storage for results or progress computed
elsewhere. See examples/progress.py.
MongoBroker distributes tasks between worker processes. A worker claims the oldest pending message for a queue with
an atomic find_one_and_update, and polls again when the queue is empty. A claimed message that isn't acknowledged
within its queue's visibility timeout (worker crashed or hung) is returned to the queue automatically, or moved to the
"dead" status once the queue's retry limit is exceeded.
from taskiq_mongodb import MongoBroker
broker = MongoBroker(
"mongodb://root:password@localhost:27017", # pragma: allowlist secret
"my_app",
queues="taskiq", # default; see Multiple queues below
collection_name="taskiq_messages", # default
)Acknowledging a message deletes its document, so a healthy queue collection stays small.
Pass a queue name, a single queue configuration, or a sequence of either to queues=. Each queue is polled by its own
background task, so different queues can have different poll intervals, visibility timeouts and retry limits without
affecting each other:
from taskiq_mongodb import MongoBroker, MongoQueue
broker = MongoBroker(
"mongodb://root:password@localhost:27017", # pragma: allowlist secret
"my_app",
queues=[
"default",
MongoQueue(name="critical", poll_interval=0.1, visibility_timeout=30),
MongoQueue(name="reports", visibility_timeout=600, max_retries=3),
],
)The first queue ("default" above) is used for tasks that don't say otherwise. Route a task to a different queue with
the queue_name label:
await add_one.kicker().with_labels(queue_name="critical").kiq(1)Kicking a task to a queue that isn't configured on the broker raises UnknownQueueError (from broker.kick() directly;
through .kiq(), as above, TaskIQ wraps it in SendTaskError with UnknownQueueError as the cause).
Set the priority label (an integer, default 0) to have a task claimed before lower-priority ones waiting in the same
queue:
await add_one.kicker().with_labels(priority=10).kiq(1)Set the delay label (seconds) to make a task claimable only after that delay has passed:
await add_one.kicker().with_labels(delay=30).kiq(1) # claimable in 30 secondsMongoBroker has two independent, complementary retry mechanisms:
- Crash recovery — every queue has a
visibility_timeout(default 300s) andmax_retries(default 0). If a worker claims a message and crashes before acknowledging it, the message becomes claimable again once the timeout passes. Aftermax_retriessuch claims it's moved to the "dead" status instead of being requeued.max_retries=0(the default) dead-letters after the very first unacknowledged attempt; there's no dedicated "unlimited" value — pass a very large number instead. - Application-level retries — for a task that raises an exception (as opposed to a worker that crashes),
use TaskIQ's own
SimpleRetryMiddlewareto re-kick it a bounded number of times:
from taskiq.middlewares import SimpleRetryMiddleware
broker = broker.with_middlewares(SimpleRetryMiddleware(default_retry_count=3))
@broker.task(retry_on_error=True, max_retries=3)
async def flaky_task() -> None: ...See examples/dead_letter.py for a full runnable example of the second kind.
Tasks can report their own progress while running, using TaskIQ's ProgressTracker, and a caller can poll it
through the result backend while the task is still executing:
from taskiq import TaskiqDepends
from taskiq.depends.progress_tracker import ProgressTracker, TaskState
@broker.task
async def process_batch(total: int, tracker: ProgressTracker[int] = TaskiqDepends()) -> str:
for done in range(1, total + 1):
...
await tracker.set_progress(TaskState.STARTED, meta=done)
return f"processed {total} items"
task = await process_batch.kiq(total=5)
progress = await task.get_progress() # -> TaskProgress(state=..., meta=...) or NoneSee examples/progress.py for the full picture, including the polling loop.
MIT — see LICENSE.