Skip to main content
Archived

This project has been archived by its maintainers, and is no longer receiving any updates.

dmq

--> Distributed Message Queue,

fully python-native, based on pluggable architecture and combination of parallelism & concurrency


install

pip install dmq

# with redis broker/backend
pip install dmq[redis]

# with postgres backend
pip install dmq[postgres]

# everything
pip install dmq[all]

quickstart

define a manager and register tasks:

# app.py
from dmq import QManager
from dmq.brokers.redis import RedisBroker
from dmq.backends.redis_backend import RedisResultBackend
from dmq.serializers.msgpack import MsgpackSerializer

broker = RedisBroker(redis_url="redis://localhost:6379")
backend = RedisResultBackend(redis_url="redis://localhost:6379")

q = QManager(
	broker=RedisBroker(redis_url="redis://localhost:6379"), 
	result_backend=RedisResultBackend(redis_url="redis://localhost:6379"), 
	serializer=MsgpackSerializer()
)
# tasks.py
from app import q

@q.register
async def add(x: int, y: int) -> int:
    return x + y

@q.register(qname="emails.send")
async def send_email(to: str, body: str) -> None:
    ...

enqueue from anywhere:

from tasks import add

task = await add.q(2, 3)
result = await task.result(timeout=10.0)  # 5

run the worker:

dmq --broker app:q --tasks tasks --workers 4

architecture

arch

everything is a protocol — swap any layer without touching the rest.


brokers

the broker handles task queuing and delivery.

broker use case backend
InMemoryBroker dev/testing asyncio.Queue
RedisBroker production redis lists + sorted sets
from dmq.brokers.memory import InMemoryBroker
from dmq.brokers.redis import RedisBroker

# in-memory, no deps
broker = InMemoryBroker()

# redis, production
broker = RedisBroker(redis_url="redis://localhost:6379", poll_interval=0.5)

result backends

store and retrieve task results.

backend use case storage
InMemoryResultBackend dev/testing dict
RedisResultBackend production redis keys with ttl
PostgresResultBackend production, durable postgres with schema + triggers
from dmq.backends.redis_backend import RedisResultBackend
from dmq.backends.postgres_backend import PostgresResultBackend

redis_backend = RedisResultBackend(
    redis_url="redis://localhost:6379",
    default_ttl=3600,
    type_serialization=True,  # preserves result types via FQN
)

pg_backend = PostgresResultBackend(
    dsn="postgresql://user:pass@localhost/mydb",
    type_serialization=True,
)
await pg_backend.connect()

type_serialization round-trips custom types (msgspec structs, etc) by storing the fully qualified name alongside the result. requires worker and client to share the same codebase.

serializers

serializer format notes
MsgpackSerializer binary default, fast
JsonSerializer text human readable
PickleSerializer binary deprecated, legacy only

scheduling

one-shot scheduling

delay, eta, or cron... fire once:

from datetime import datetime, UTC, timedelta

# delay by 30 seconds
await my_task.sched(delay=30.0, arg1, arg2)

# fire at a specific time
await my_task.sched(eta=datetime(2025, 6, 1, tzinfo=UTC))

# next cron match
await my_task.sched(cron="0 */4 * * *")

periodic scheduling

register tasks that fire on a recurring schedule for the lifetime of the program:

from dmq.types import CronSchedule, DelaySchedule

# every 5 minutes
@q.periodic(DelaySchedule(delay_seconds=300))
async def heartbeat():
    ...

# every 4 hours, cron
@q.periodic(CronSchedule(cron_expr="0 */4 * * *"))
async def cleanup():
    ...

# every 4 hours, but first fire delayed 1 hour from program start
@q.periodic(CronSchedule(cron_expr="0 */4 * * *", delay_seconds=3600))
async def sync_data():
    ...

periodic tasks are started automatically when QWorkerPool.start() is called. the PeriodicScheduler runs as a background asyncio task, computing next fire times and enqueuing via broker.send_task() at each tick.

delay_seconds on CronSchedule offsets the base time forward before computing the next cron match — useful for staggering periodic work or deferring from program start.


delivery guarantees

from dmq.guarantees import DeliveryConfig, DeliveryGuarantee

config = DeliveryConfig(
    guarantee=DeliveryGuarantee.AT_LEAST_ONCE,  # default
)
guarantee behavior
AT_MOST_ONCE ack before processing — fast, may lose tasks
AT_LEAST_ONCE ack after processing — default, may reprocess
EXACTLY_ONCE ack after + idempotency store — requires enable_idempotency=True

retry policies

from dmq.retry import RetryPolicy, ExponentialBackoff, LinearBackoff, FixedDelayBackoff

policy = RetryPolicy(
    max_retries=5,
    backoff=ExponentialBackoff(base=2.0, max_delay=3600.0, jitter=True),
    retry_on=(ConnectionError, TimeoutError),
    dont_retry_on=(ValueError,),
)

backoff strategies: ExponentialBackoff, LinearBackoff, FixedDelayBackoff


worker pool

from dmq import QWorkerPool, ExecutionMode

pool = QWorkerPool(
    manager=q,
    worker_count=4,
    max_tasks_per_worker=10,
    execution_mode=ExecutionMode.ASYNC_ONLY,
)

await pool.start()
await pool.run_forever()

execution modes

mode python build worker type recommended workers
ASYNC_ONLY standard cpython QAsyncWorker min(cpu * 2, 8)
THREADED free-threaded 3.13+ QThreadedWorker cpu_count

auto-detected at runtime via detect_execution_mode(). free-threaded mode requires sys._is_gil_enabled() == False.


events & callbacks

hook into task lifecycle:

from dmq.callback import Callback, CallbackRule
from dmq.events import QEventType

@q.callback
class LogFailures(Callback):
    rules = [CallbackRule(event_types=[QEventType.TASK_FAILED])]

    async def on_event(self, event):
        print(f"task {event.task_id} failed: {event.exception}")

event types:

  • TASK_QUEUED
  • TASK_STARTED
  • TASK_COMPLETED
  • TASK_FAILED
  • TASK_RETRY
  • TASK_NOT_FOUND

partitioning

from dmq import HashPartitionStrategy, KeyBasedPartitionStrategy, RoundRobinPartitionStrategy

strategy = HashPartitionStrategy(num_partitions=8)
partition = strategy.get_partition("task-id-123")

cli

dmq --broker app:q --tasks tasks --workers 4 --concurrency 20 --guarantee at_least_once

# options
  --broker        		python path to QManager instance [module:variable]
  --tasks         		task modules to import [space separated]
  --workers, -w			number of workers [default: auto]
  --concurrency, -c  	max concurrent tasks per worker [default: 10]
  --guarantee, -g    	[at_most_once | at_least_once | exactly_once]
  --execution-mode, -e  [auto | async | threaded]
  --log-level, -l    	[debug | info | warning]

development

# install
uv sync --all-extras --dev

# test
just test

# test with testcontainers (redis + postgres)
just test-lima

# lint & format
just lint

# type check
just type-check

license

apache 2.0 - see here

Metadata

Release files for dmq 0.1.0.post1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dmq 0.1.0.post1
File Size Uploaded Provenance Actions
dmq-0.1.0.post1.tar.gz 25.3 kB Without provenance Details for dmq-0.1.0.post1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for dmq 0.1.0.post1
File Interpreter ABI Platform Actions
dmq-0.1.0.post1-py3-none-any.whl Python 3 none any Details for dmq-0.1.0.post1-py3-none-any.whl

Total release size: 65.4 kB

Release files / dmq-0.1.0.post1.tar.gz

Download URL dmq-0.1.0.post1.tar.gz
Size 25.3 kB
Tags Source
SHA-256 checksum
How to use checksums
2b7117c5e709865c10d0d4ab56560e29014f82be96a830039b3ab0e9039bc20d
BLAKE2b-256 checksum
How to use checksums
c6da5e067e3cea2640bcac33f4ba909a45c904d51fe911a4ddacead7a962a768
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / dmq-0.1.0.post1-py3-none-any.whl

Download URL dmq-0.1.0.post1-py3-none-any.whl
Size 40.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7f42d4637fd9a4de0acac717dc38959cc4607d1ffd93747b07afd7c674873ee1
BLAKE2b-256 checksum
How to use checksums
f3e2ad531d9ec662a676aeb80b9e4b6b30fe073f4c1a9e6898654dd04118d248
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for dmq 0.1.0.post1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page