Skip to content

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[0.4.0] - 2026-08-15

Changed

  • BREAKING: _kombu.binding.{exchange} is now a sorted set instead of a plain set, scored with the unix time each binding goes stale. Bindings were never removed: Redis cannot expire an individual member, and only queue_delete removes one, which reaches just the bindings the calling process declared itself. So the routing table of a long-lived exchange grew for the life of the deployment, and a celery control client, which binds a fresh reply queue per call and does not always get to unbind it, drove that growth. The deadline is x-expires after the last refresh and never less than MIN_BINDING_LIFETIME (300 seconds); a queue without x-expires is scored +inf and still only goes away on an explicit unbind. Declaring, refreshing and publishing all rescore, and get_table drops whatever has aged out before it reads, so cleanup rides the read path and nothing has to sweep. Rescores use ZADD GT, so a channel with a short x-expires window cannot pull back a deadline that a channel with a longer window pushed further out. The first _queue_bind converts an inherited set in place, keeping every member and scoring it +inf. The conversion is one-way and the key name is shared with kombu's own Redis transport, so the two can no longer declare against the same exchange: run celery.contrib.migrate.migrate_tasks before deploying this version, or DEL the binding keys if you already did. See the migration guide
  • BREAKING: the involuntary-redelivery cap now follows RabbitMQ quorum queues. The max_restore_count transport option is now delivery_limit, the restore_count message hash field is now delivery_count, and the x-restore-count header is now x-delivery-count. The default changed from no limit to 20, which is what RabbitMQ quorum queues have applied since 4.0, and the counter now counts delivery attempts rather than redeliveries, so a message is dropped on its 20th delivery. Set delivery_limit: None in broker_transport_options to keep the old unlimited behaviour. A message published by an older version has no delivery_count field, which reads as 0, so it simply starts over
  • BREAKING: fanout bindings are no longer written to _kombu.binding.{exchange}. Fanout routing never reads the table: publishing is one XADD to the exchange's stream, and consumers follow the streams their own channel subscribed to, so the members only piled up. kombu's generic exchange_delete and list_bindings no longer see fanout bindings; a binding key left behind by an earlier version is deleted on the first fanout declare of the new version, since nothing would ever read or rescore it again
  • Channel.enqueue_due_messages now returns a SweepStats NamedTuple (enqueued, dropped, redelivered, orphaned) instead of a bare count

Added

  • blocking_timeout transport option (default 10), the seconds BZMPOP and XREAD block on the server per poll. This was polling_interval, which in kombu means the sleep between unsuccessful polls, so one attribute drove two opposite mechanisms: kombu.transport.virtual.Transport.drain_events slept 10 seconds after any poll that came up empty, and the sleep was clamped to the caller's drain timeout rather than skipped. kombu's own Redis transport sets polling_interval = None and keeps brpop_timeout separate for exactly this reason, and this transport now does the same. Setting polling_interval still works, read as blocking_timeout with a deprecation warning and with the sleep left disabled. Keep it below socket_timeout if you set one; 0 is passed through as-is and blocks each poll until a message arrives
  • queue_expires transport option (default None): expiry in seconds for every queue declared without its own x-expires. With it set, binding tables and fanout streams carry TTLs too, refreshed by the same declares, publishes and periodic refreshes that keep queues alive, so an abandoned deployment's queues, indexes, binding tables and streams all expire on their own. Message hashes are the exception: they follow message_ttl/x-message-ttl only, and an expired index leaves them unreachable, so pair the two options if unconsumed payloads must not outlive their queue. A per-queue x-expires still wins and the 10-second floor applies. Binding-key TTLs only ever grow (PEXPIRE GT), so a queue with a short window cannot cut down what another queue's touch pushed out. Set it deployment-wide: a process without the option neither writes nor refreshes these TTLs, so its routes could expire from under it

Documentation & Diagnostics

  • Documented the sep transport option, which was accepted but never listed. A deployment migrating from the standard Redis transport has to carry over whatever sep it configured there, because _kombu.binding.{exchange} is the one piece of broker state the two transports share a key name for
  • Added a "Carry over a custom sep" section to the migration guide covering both failure modes of a mismatch: kombu raising ValueError: not enough values to unpack (expected 3, got 1) on every publish, and this transport padding the member to (member, "", "") so routing silently matches nothing
  • get_table now logs a warning (once per process) naming the exchange and the offending member when a binding does not split into three parts. Padding behaviour is unchanged, so nothing starts raising
  • get_table now names the abandoned bindings it prunes (INFO), so an aged-out route can be told apart from one that never existed
  • The requeue sweep now reports what it did. Messages dropped at the delivery limit are named in the error log (task name and id, up to 10 per queue per sweep); the drop deletes the message hash, so that log line is the last trace of the message. Redeliveries and orphaned index entries are counted and logged at INFO

Fixed

  • Publishing to a durable direct exchange whose binding table is empty now raises InconsistencyError instead of discarding the message. kombu made the empty table a silent no-op in 5.2 (PR #1404), which is right for topic and fanout but not for durable direct, where the binding is known to exist and, with x-expires, may simply have aged out. InconsistencyError is in connection_errors, so kombu redeclares the binding and retries. The visible symptom was pidbox replies vanishing after a control queue expired. A transient direct exchange keeps kombu's drop, with an INFO log: a pidbox reply exchange loses its bindings the moment its control client leaves, and the publisher redeclaring its own entities cannot recreate a binding that belonged to someone else, so raising there only churned through a pointless retry loop
  • x-expires and x-message-ttl now apply to publishes made on a channel that did not declare the queue itself. kombu caches declarations per connection, so only the first channel to declare a queue ever sees its arguments, while any channel of that connection may be the one publishing. The TTL registries are now shared by all channels of a connection instead of being per-channel
  • Acking a message now removes it from queue:{name} as well as from messages_index:{name}. A message whose visibility timeout had already restored it left the restored copy behind, so it was delivered again after being acked
  • A consumed message always gets a visibility deadline. Both consume paths refreshed the index entry with ZADD ... XX, which is a no-op when the entry is gone, so such a message was never recovered if its worker died
  • A queue backlog is no longer counted as a redelivery. enqueue_due_messages gates the counter on the ZADD NX result, so a message still sitting in its queue past its deadline is re-dated but neither counted nor dropped. Without this, a queue slower than visibility_timeout would have eaten its own backlog once delivery_limit gained a default
  • delivery_info["redelivered"] and the x-delivery-count header are now derived from the delivery counter at consume time. redelivered used to be a hash field that was written but never read, so Celery's worker_deduplicate_successful_tasks never saw a redelivery. The header goes into the message's top-level headers map, which is where kombu reads headers from when it rebuilds a message; properties["headers"], where it went first, never reaches the consumer
  • Messages consumed with no_ack (pidbox control and reply queues, and basic_get(..., no_ack=True)) are now dequeued inside the atomic pop instead of being given a visibility deadline. Nothing ever acks a no_ack delivery, so its index entry and hash survived until the requeue sweep re-enqueued the message on its deadline, and a control command could fire a second time visibility_timeout later
  • x-expires is now refreshed on connections that have no event loop. The refresh only ever ran off a timer inside a worker's hub, so a celery control client waiting for replies, a Flower event receiver and a gevent worker's synloop all let their own queues, and now their bindings, age out from under them. They drain events instead, so the drain path refreshes at the same interval the timer would have used
  • The queue expires refresh timer now starts for queues declared before the event loop existed. register_with_event_loop never called _update_expires_timer after attaching the loop, so a worker that declared all its queues at startup refreshed none of their TTLs and its queues expired underneath it
  • QoS.restore_unacked_once no longer shuts the worker thread pool down on broker reconnects. kombu calls it from Channel.close(), which also runs when the consumer reconnects, so every broker blip permanently disabled the pool (later submit() calls raised RuntimeError while the worker kept answering inspect ping). It is now gated on the worker blueprint having entered CLOSE/TERMINATE
  • Reconnects no longer requeue messages whose tasks are still running. Those messages stay in messages_index and are redelivered on their visibility deadline instead
  • Worker lookup no longer relies on channel.connection.client.app, which never resolves (kombu's Connection has no app attribute) and made the lookup raise AttributeError on every call
  • The heartbeat, ack cleanup and requeue paths now track the queue a message was consumed from instead of assuming delivery_info["routing_key"] names it. kombu stamps the publish-time routing key into delivery_info and never rewrites it on delivery, so for a queue bound under a routing key that is not its name the heartbeat pushed the deadline of a nonexistent index entry and a long-running task was redelivered mid-run, acking left the real index entry (and a restored queue copy) behind, and reject-with-requeue looked up the per-queue x-message-ttl under the wrong name. The consume paths now record the queue in delivery_info["queue"] at pop time
  • delivery_limit is now enforced on reject-with-requeue too. The requeue script counted the redelivery but left the drop to the requeue sweep, and a live reject loop re-stamps the index deadline on every consume, so the sweep never saw the entry come due and the message bounced at the front of its queue forever. The requeue script now drops at the limit with the sweep's attempt counting; the dropped message is named in the error log, which is its last trace. Like the sweep, it gates the count on the ZADD NX result, so a reject arriving after the sweep already restored the same delivery neither counts it a second time nor drops a message the limit still allows
  • Timed-out and delayed messages are now recovered on connections that have no event loop. The requeue sweep and the visibility heartbeat only ever ran off timers inside a worker's hub, so a gevent or eventlet worker never restored a crashed worker's messages, never delivered native-delayed messages, and let its own in-flight messages hit their visibility deadline mid-task. The drain path now runs both at the intervals the timers would have used, as it already did for the x-expires refresh
  • Cancelling the last consumer while a poll is in flight no longer wedges the channel. The reply that arrived after the cancel left the channel claiming a command was still on the wire when none was, so the next basic_consume never started a poll and the consumer starved, and closing the channel blocked forever waiting for the phantom reply. Consuming again after such a spell also restarts in the atomic FAST mode: the non-atomic BZMPOP path is only used straight after FAST confirmed the queues empty, a fact an idle spell no longer vouches for
  • The visibility heartbeat no longer skips a channel with no consumers. Unacked deliveries outlive consumption: after cancel_consumer, a still-running acks_late task kept its message in flight but the heartbeat stopped pushing its deadline, so the message hit its visibility timeout mid-run and another worker started it a second time

[0.3.0] - 2026-02-14

Added

  • Queue TTL (x-expires): queues auto-expire when no worker refreshes them, via periodic PEXPIRE with dynamic interval (TTL/2)
  • Message TTL (x-message-ttl): per-queue message expiry via shorter EXPIRE on message hashes
  • prepare_queue_arguments override using kombu's to_rabbitmq_queue_arguments for RabbitMQ-compatible queue argument handling

Changed

  • Split global messages_index sorted set into per-queue messages_index:{queue} keys for scoped recovery, clean queue lifecycle, and correct global_keyprefix behavior with Lua scripts
  • Renamed internal redis-specific naming to client-library-agnostic (client_lib, _client_exceptions) for better redis-py/valkey-py compatibility
  • Default message TTL changed from 3 days to -1 (no TTL); configurable via message_ttl channel attribute
  • CI/CD: tag workflow now gates on CI success instead of running on every push

Fixed

  • EXPIRE and PEXPIRE commands now correctly prefixed when global_keyprefix is set
  • _bzmpop_read and _get now skip expired message hashes and try the next message instead of raising Empty
  • x-expires below minimum (10s) now clamped with warning instead of raising ValueError
  • Removed redundant redis-specific getter functions (get_redis_error_classes, get_redis_ConnectionError, _get_response_error)

[0.2.5] - 2026-02-14

Fixed

  • Fanout/broadcast (events, Flower) now works: added dedicated subclient for XREAD and fixed per-routing-key stream splitting

Added

  • Example project in examples/simple/ demonstrating tasks, delayed delivery, priority, retries, and Flower

[0.2.4] - 2026-01-31

Added

  • Migration support from standard Redis transport

Fixed

  • Simplified transport configuration in docs

[0.2.3] - 2026-01-29

Added

  • Support for both redis-py and valkey-py client libraries (optional dependencies)
  • valkey:// and valkeys:// URL scheme support for easier configuration
  • SSL/TLS detection from valkeys:// URL scheme
  • Priority clamping for out-of-range values (clamps to 0-255 range with warning)

Fixed

  • Documentation site 404 by setting dev as default version

[0.2.2] - 2025-01-22

Changed

  • Updated celery-types-ng to 0.25.4 and fixed typing errors

[0.2.1] - 2025-01-21

Changed

  • Added queue: prefix to avoid collision with list-based queues

[0.2.0] - 2025-01-20

Added

  • Native delayed delivery support
  • Full priority support (0-255)
  • Reliable fanout via Redis Streams
  • Visibility timeout tracking

Changed

  • Switched from Redis lists to sorted sets for queues
  • Improved message reliability with per-message hashes

[0.1.0] - 2025-01-15

Added

  • Initial release
  • Custom Kombu transport for Redis/Valkey
  • Basic queue operations with sorted sets