3.0.0
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.
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.startRemoved 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)
endSingle-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.newsignature has changed: the third positional argument is nowopts = {}(an option hash) instead of a consumer work pool.
UseBunny::Session#create_channelor pass the work pool asopts[:work_pool]VersionedDeliveryTagremoved: delivery tags are now raw integersConsumer#recover_from_network_failurewas removed: topology recovery is now handled byBunny::SessionviaTopologyRegistryExchange#recover_from_network_failurewas removed: see aboveQueue#recover_from_network_failureandQueue#recover_bindingswere removed: see above