Skip to content

3.0.0

Choose a tag to compare

@michaelklishin michaelklishin released this 01 Apr 00:14
· 15 commits to main since this release
280e2eb

Changes between Bunny 2.24.0 and 3.0.0 (March 31, 2026)

Topology Recovery Improvements

Back in 2013-2014, RabbitMQ Java client's connection recovery was heavily
influenced by what was available in Bunny.

In 2025, Bunny starts adopting the features Java client has developed
over the years, starting with connection-level topology tracking.

Now all exchanges, queues, bindings recorded for topology recovery are stored
and maintained by Bunny::Session and not Bunny::Channel. This makes
recovery somewhat simpler and eliminates a class of problems where,
say, a queue was declared on one channel, deleted on another, and re-created
by Bunny's connection recovery contrary to the user's intent.

The only potentially breaking change here is: the client now intentionally skips
tracking queue and exchange declarations with passive: true.

GitHub issues: #704, #711

Reduced Connection Recovery Logging

On connections that have connection recovery enabled, certain I/O exceptions
are now logged at debug level to reduce log noise.

GitHub issue: #711

Topology Recovery Filters

class ExampleTopologyFilter < Bunny::TopologyRecoveryFilter
  def filter_queues(qs)
    qs.filter { |rq| rq.name.start_with?(/^filter-me/) }
  end

  def filter_exchanges(xs)
    xs.filter { |rx| rx.name.start_with?(/^filter-me/) }
  end

  def filter_queue_bindings(bs)
    bs.filter { |rb| rb.destination.start_with?(/^filter-me/) }
  end

  def filter_exchange_bindings(bs)
    bs.filter { |rb| rb.destination.start_with?(/^filter-me/) }
  end

  def filter_consumers(bs)
    bs.filter { |rc| rc.consumer_tag.start_with?(/filter-me/) }
  end
end

tf = ExampleTopologyFilter.new
# Will use the above filter to determine what queues, exchanges, bindings,
# and consumers must be retained (recovered) during topology recovery
c = Bunny::Session.new(topology_recovery_filter: tf)
c.start

Removed Versioned Delivery Tags

Versioned delivery tags introduced about as many problems as they have solved.

Originally introduced in 2013 shortly after automatic connection recovery,
they have been a polarizing feature for years.

3.0 is a good opportunity to remove them.

GitHub issue: #700.

Significant Publisher Performance Improvements

Publisher performance improvements (100K messages, with amq-protocol 2.4.0 or later)
with automatic publisher confirm tracking enabled (documented below):

Approach Throughput vs 2.x confirms
2.x wait_for_confirms ~11k msg/s baseline
3.x single publish ~35k msg/s 320%
3.x basic_publish_batch(500) ~43k msg/s 390%
3.x basic_publish_batch(1000) ~45k msg/s 410%
3.x basic_publish_batch(2000) ~44k msg/s 400%
3.x basic_publish_batch(3000) ~43k msg/s 390%

Bunny 3.0's confirm tracking is 3-4x faster than 2.x. Batch size of 1000
provides optimal throughput. Avoid batches over 3000 (they will perform worse due to
connection flow control on the RabbitMQ end).

To migrate from 2.x, simply replace Channel#confirm_select calls with Channel#confirm_select(tracking: true).
That's it.

In addition, Bunny::Channel#basic_publish_batch benefits further from the write hot path optimizations
that do not benefit Bunny::Channel#basic_publish much.

Publisher Confirm Tracking

Bunny now supports publisher confirm
tracking, inspired by the .NET client 7.x
and Swift Bunny.

Use basic_publish_batch for optimal throughput (batch sizes of 500-3000 recommended):

ch.confirm_select(tracking: true)

messages.each_slice(1000) do |batch|
  ch.basic_publish_batch(batch, "", queue.name)
end

Single-message publishing is also supported but slower:

ch.confirm_select(tracking: true)
messages.each { |msg| x.publish(msg, routing_key: q.name) }

When tracking is set to true, outstanding_limit defaults to 1000 (this is an optimal value according to the benchmarks, see below).
This provides backpressure when too many messages are unconfirmed.

If the broker nacks a message, a Bunny::MessageNacked exception is raised.

Performance (100K messages, with amq-protocol 2.7.0):

Approach Throughput vs 2.x confirms
2.x wait_for_confirms ~11k msg/s baseline
3.x single publish ~35k msg/s 320%
3.x basic_publish_batch(500) ~43k msg/s 390%
3.x basic_publish_batch(1000) ~45k msg/s 410%
3.x basic_publish_batch(2000) ~44k msg/s 400%
3.x basic_publish_batch(3000) ~43k msg/s 390%

Bunny 3.0's confirm tracking is 3-4x faster than 2.x. Batch size of 1000
provides optimal throughput. Avoid batches over 3000 (they will perform worse due to
connection flow control on the RabbitMQ end).

To migrate from 2.x, simply replace Channel#confirm_select calls with Channel#confirm_select(tracking: true).
With that single line you get automatic backpressure via publisher confirms and three times better throughput.

Important design note: unlike the .NET client 7.x and Swift Bunny, which both pause the caller per-message using the async/await
features in those languages (this is very cheap: just suspends a task), Bunny in Ruby uses a watermark approach
with a shared condition variable. This avoids per-message mutex contention that has a dramatic negative performance effect.

Consumer Delivery Performance Optimizations

Several optimizations to reduce overhead in the consumer delivery hot path:

  • DeliveryInfo: hash representation is now lazily created only when accessed via
    to_hash, each, or []; direct method access (e.g., delivery_tag, routing_key)
    no longer allocates a hash, providing a roughly x2 speedup on microbenchmarks of very simplistic consumers

  • Consumer lookup caching: channels now cache the last consumer lookup, benefiting
    the common single-consumer-per-channel pattern

  • Frame header buffer reuse: the transport layer now reuses a buffer when reading
    frame headers, reducing per-frame allocations

Exchange Type Constants

Bunny::Exchange now provides constants for all built-in and commonly used
exchange types: TYPE_DIRECT, TYPE_FANOUT, TYPE_TOPIC, TYPE_HEADERS,
TYPE_MODULUS_HASH, TYPE_LOCAL_RANDOM, TYPE_CONSISTENT_HASH, TYPE_RANDOM.

Tanzu RabbitMQ Delayed Queue Support

Bunny::Queue::Types::DELAYED and Channel#delayed_queue declare a
Tanzu RabbitMQ delayed queue with optional :delayed_retry_type,
:delayed_retry_min, and :delayed_retry_max options.

Tanzu RabbitMQ JMS Queue Support

Bunny::Queue::Types::JMS and Channel#jms_queue declare a
Tanzu RabbitMQ JMS queue with optional :selector_fields and
:selector_field_max_bytes options.

Channel#reopen

A new method that reopens a channel after a server-initiated closure
(e.g. due to a consumer delivery acknowledgement timeout or an unknown delivery tag).
The channel is reopened on the same connection, reusing its original channel id,
and its prefetch, confirm, and transactional settings are recovered.

Session#recover_channel_topology

Recovers topology (exchanges, queues, bindings, consumers) for a single channel.
Intended for use after Channel#reopen.

amq-protocol Bumped to 2.7.0 (or Later)

Bunny now requires amq-protocol 2.7.0 or later for the Channel::Close
predicate methods (#unknown_delivery_tag?, #delivery_ack_timeout?, #message_too_large?)
that Bunny used to reinvent (with regular expression matches on reply_text)

Limit Hostname Resolution Time

Bunny now configures its TCP socket to limit the hostname resolution time,
assuming that the OS kernel supports the underlying socket option.

Breaking Changes

Except for the VersionedDeliveryTag removal, all breaking changes in this release are minor and
do not affect most codebases that use Bunny.

  • Bunny::Channel.new signature has changed: the third positional argument is now opts = {} (an option hash) instead of a consumer work pool.
    Use Bunny::Session#create_channel or pass the work pool as opts[:work_pool]
  • VersionedDeliveryTag removed: delivery tags are now raw integers
  • Consumer#recover_from_network_failure was removed: topology recovery is now handled by Bunny::Session via TopologyRegistry
  • Exchange#recover_from_network_failure was removed: see above
  • Queue#recover_from_network_failure and Queue#recover_bindings were removed: see above