CLI tool
ruby-amqp/hutch avatar
ruby-amqp/hutch

Hutch: a conventions-based Ruby layer over RabbitMQ for service-to-service messaging

A system for processing messages from RabbitMQ.

876 stars144 forksRubyMIT

At a glance

What is it?
Hutch is a Ruby library that turns RabbitMQ into a background job system for services: consumers live in their own files and include a module, publishers call two methods, and a command line runner wires up queues and bindings. It is opinionated by design, it dropped support for older Rubies and RabbitMQ versions in v2.0.0, and it has been maintained since GoCardless extracted it from production in 2013.
Who is it for?
Adopt Hutch when your Ruby services need to talk over RabbitMQ and you would rather not write queue setup, binding and retry handling yourself, and you are willing to accept topic exchanges as the distribution model. Do not adopt it if your messaging topology needs direct exchanges or headers exchanges, or if you are still on Ruby below 3.2 or RabbitMQ below 3.13.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 54 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What Hutch is and which problems it deliberately removes

Hutch is a Ruby library for enabling asynchronous inter-service communication in a service-oriented architecture, using RabbitMQ. The README calls it a conventions-based framework and is explicit that it is opinionated: it uses topic exchanges for message distribution and makes assumptions about how consumers and publishers should work.

That framing explains the shape of the API. A consumer is a class in its own file that includes the Hutch::Consumer module and defines a process method. The class declares which topics it wants with a consume call, and the framework derives the queue name, sets up the binding and runs the loop. A publisher calls Hutch.connect and then Hutch.publish. Neither side has to know the exchange topology, the queue names or the connection details.

The requirements are stated up front and they are strict for a library this old: Ruby 3.2 or later, or JRuby 10 or later, and RabbitMQ 3.13 or later. The Ruby floor in particular is a signal about the v2.0.0 release, which arrived on 2026-08-27 after a release candidate a few days earlier and after v1.4.0 from 2026-04-08. Supporting old Ruby and old RabbitMQ at the same time is what the older release line was for.

Under the hood, Hutch uses Bunny, or March Hare on JRuby. Those are the AMQP client libraries, and knowing which one is in play matters because the connection options, the TLS handling and the reconnection behaviour you inherit are theirs, not Hutch's.

Writing a consumer: routing keys, queue names and the message object

A consumer needs three things: the module, a topic, and a process method. The routing key goes into consume, and the process method receives a Message object.

ruby
class FailedPaymentConsumer
  include Hutch::Consumer
  consume 'gc.ps.payment.failed'

  def process(message)
    mark_payment_as_failed(message[:id])
  end
end

The Message object carries the message data plus any associated metadata, and you read properties with Hash-style indexing, so message[:id] gives you the value of the id key. That is the entire data access surface, and it is deliberately small: the payload arrives as a hash rather than as a domain object, so the consumer is responsible for turning it into whatever the application needs.

Multiple routing keys are handled by passing extra arguments to consume, and the README points at the RabbitMQ topic exchange tutorial for how routing keys are matched. If your consumer wants to listen on several patterns, you list them and RabbitMQ does the matching.

The queue name defaults to the consumer class name, which is convenient until you want to rename a class or keep a stable queue across a refactor. The queue_name method sets it explicitly:

ruby
class FailedPaymentConsumer
  include Hutch::Consumer
  consume 'gc.ps.payment.failed'
  queue_name 'failed_payments'

  def process(message)
    mark_payment_as_failed(message[:id])
  end
end

For a payment failure consumer, a queue called failed_payments survives a rename of the class, which is what you want for a queue that already holds messages.

Queue options: quorum queues, lazy mode and custom arguments

Hutch passes through the RabbitMQ queue options that decide how durable and how replicated a queue is, which is where the operational decisions live.

Two methods cover the common cases. lazy_queue puts the queue into lazy mode, and quorum_queue makes it a quorum queue with an optional initial_group_size argument:

ruby
class FailedPaymentConsumer
  include Hutch::Consumer
  consume 'gc.ps.payment.failed'
  queue_name 'failed_payments'
  lazy_queue
  quorum_queue initial_group_size: 3

  def process(message)
    mark_payment_as_failed(message[:id])
  end
end

A quorum queue replicated across three nodes is a different durability story from a classic mirrored queue, and lazy mode defers keeping messages on disk until they need to be. Both choices belong to the consumer class, which means the durability of a queue is decided next to the code that consumes it.

Anything beyond that goes through the arguments method, which sets RabbitMQ queue arguments directly. The README's example caps a queue at ten messages:

ruby
class FailedPaymentConsumer
  include Hutch::Consumer
  consume 'gc.ps.payment.failed'
  arguments 'x-max-length' => 10
end

The README links the RabbitMQ documentation on queue length limits and on AMQP protocol extensions for the available arguments. Note the operational consequence of x-max-length: once the queue is full, RabbitMQ drops or rejects messages according to the overflow policy. A consumer that sets a limit and a handler that assumes every message eventually arrives are making contradictory assumptions, and that is a decision to make deliberately rather than copy.

Producers, publisher confirms and the rules Hutch asks of you

On the producing side, publishers connect with Hutch.connect and publish with Hutch.publish. The README has a section on publisher confirms and another on writing well-behaved publishers, which tells you the library cares about the delivery guarantee rather than treating it as an afterthought.

Publisher confirms are the mechanism by which a broker acknowledges receipt of a published message. Without them, a publisher that sees no error cannot tell the difference between a message that was accepted and a message that was lost on the way. For an inter-service system where one service triggers work in another, that difference decides whether you need idempotency downstream.

The configuration section covers the producer setup in detail, with a producer configuration subsection and then the config file, environment variables, configuration precedence and a generated list of configuration options. The generated list matters in practice: it means the configuration surface is documented mechanically from the code, so you can look up a key rather than read source to find out what the options are.

The precedence rules are the part worth understanding before you wire Hutch into a deployment. When the same setting can arrive from a config file, an environment variable and a direct call to Hutch::Config.set, the order decides which one wins. Getting that wrong in a containerised environment is a common way to end up talking to the wrong broker.

Running the worker and loading consumers

The gem installs a command line runner called hutch, and the README's instruction is to run hutch after installing the gem. It takes options for the broker connection, and the help output shows the shape:

console
$ hutch -h
usage: hutch [options]
        --mq-host HOST               Set the RabbitMQ host
        --mq-port PORT               Set the RabbitMQ port
    -t, --[no-]mq-tls                Use TLS for the AMQP connection
        --mq-tls-cert FILE           Certificate for TLS client verification
        --mq-tls-key FILE            Private key for TLS client verification
        --mq-exchange EXCHANGE       Set the RabbitMQ exchange
        --mq-vhost VHOST             Set the RabbitMQ vhost
        --mq-username 

TLS is a first-class set of options rather than something bolted on, with a certificate and a private key for client verification, which tells you the library expects to run against brokers that require client certificates in some deployments.

The runner connects to RabbitMQ, sets up queues and bindings, and loads the consumers. The README describes two loading modes: loading consumers by convention from separate files, and loading them manually one by one. Consumer groups are covered as a separate mechanism. The README also documents how to stop Hutch.

The repository layout backs this up. There is a bin/ directory for the executable, lib/ for the library, spec/ for the test suite and a .rspec file, templates/ for whatever Hutch generates, and an examples/ directory with a consumer example and a producer example. The gemspec, Gemfile and Rakefile are the standard Ruby packaging set, and there is a CHANGELOG.md at the root.

Logging and tracing hooks

Two extension points concern observability, and both are set through Hutch::Config.

For logging, a consumer calls the logger method, which returns a Logger object, so logging from inside a consumer is a method call rather than a global. The README's example logs from the process method:

ruby
class FailedPaymentConsumer
  include Hutch::Consumer
  consume 'gc.ps.payment.failed'

  def process(message)
    logger.info "Marking payment #{message[:id]} as failed"
    mark_payment_as_failed(message[:id])
  end
end

If you run Hutch inside Rails and want it to log to the Rails logger rather than to stdout, the README tells you to add a line to config/initializers/hutch.rb:

ruby
Hutch::Logging.logger = Rails.logger

The client side is configured separately, by setting a client_logger before calling Hutch.connect:

ruby
client_logger = Logger.new("/path/to/bunny.log")
Hutch::Config.set(:client_logger, client_logger)

That second one is worth noting because it is the underlying Bunny client that writes it, and separating the two logs is a reasonable way to distinguish transport problems from application problems.

The tracing hooks are for tracking message processing, and the README gives one line per supported APM:

ruby
Hutch::Config.set(:tracer, Hutch::Tracers::NewRelic)
ruby
Hutch::Config.set(:tracer, Hutch::Tracers::Datadog)

The README's phrase for these is that the batteries are included. That phrase undersells it slightly: a tracer that understands the message lifecycle is the difference between a queue depth graph and an actual answer to where the time goes in an asynchronous system.

Limits: topic exchanges only, and a small API surface

The clearest limit is the one the README states plainly. Hutch uses topic exchanges for message distribution, and it makes assumptions about how consumers and publishers should work. If your messaging design needs a direct exchange, a headers exchange or a fanout exchange, that is outside what the framework offers, and you would be reaching past it to Bunny directly. The routing key model is the whole point of the convention, so it is not a setting you can change.

The second limit is the small surface. The consumer API is include the module, call consume, define process, and the payload is a hash. There is no serializer registry, no middleware chain, no retry policy DSL and no dead letter routing described anywhere in the README. Every one of those concerns has to be implemented in your process method or handled at the broker level with queue arguments. That is a coherent position for a framework that wants to make one path easy, and it does mean you will write some of the resilience yourself.

The third limit is the platform floor. Ruby 3.2+ and RabbitMQ 3.13+ means the library does not run on the older stacks many production environments still have, and the v2.0.0 release is what enforced that. The January 2013 origin and the 2026 release line also mean the API you are learning has had a long time to settle, which cuts both ways: the conventions are stable, but they are conventions from 2013 that have been extended rather than redesigned.

Hutch against raw Bunny and against a managed queue service

Two comparisons are worth drawing.

The first is against using Bunny directly. Bunny is a thin, well-behaved AMQP client, and you can absolutely build a service on it: declare the exchange, declare the queue, bind, subscribe, publish. What Hutch adds is the convention layer, so that a queue name is derived from a class, the binding follows from a routing key declared next to the handler, and the runner handles startup and shutdown. The cost is that you have accepted the framework's model to get that, and the model is opinionated by construction. Hutch is worth it when you have many consumers and want them to look the same; if you have one queue and one message type, Bunny alone is less machinery.

The second comparison is against a managed queue service. A hosted broker removes the operations: someone else runs the nodes, patches the broker and handles replication. Hutch does not change what your code has to do to publish and consume, but it does sit next to a managed broker very naturally, because the queue options it exposes, quorum queues with a group size and lazy mode, are exactly the knobs a hosted RabbitMQ provider exposes. Hutch also works with a broker you run yourself, which the Bunny underneath supports.

The deciding question is where the RabbitMQ sits. If you already run RabbitMQ in your infrastructure, Hutch is a clear improvement over hand-rolled Bunny code. If you would rather not run a broker at all, a managed queue service with its own SDK may serve you better, since Hutch's value is in the conventions around a broker you operate rather than in the messaging itself.

Editorial conclusion

Adopt Hutch when your Ruby services need to talk over RabbitMQ and you would rather not write queue setup, binding and retry handling yourself, and you are willing to accept topic exchanges as the distribution model. Do not adopt it if your messaging topology needs direct exchanges or headers exchanges, or if you are still on Ruby below 3.2 or RabbitMQ below 3.13. Verify first that your broker version and Ruby version clear the stated requirements, that the routing keys you plan to publish match the consume calls in your consumers, and that the queue options you pass are ones your RabbitMQ deployment supports.

Frequently asked questions

What are the requirements for running Hutch?

The README states that Hutch requires Ruby 3.2 or later, or JRuby 10 or later, and RabbitMQ 3.13 or later. Those floors were introduced with the v2.0.0 release, which followed v1.4.0 in April 2026.

How do I write a consumer in Hutch?

Include the Hutch::Consumer module in a class, declare the routing keys with consume, and define a process method that takes a Message object. You read message properties with hash-style indexing, for example message[:id], and the queue name defaults to the consumer class name unless you set it with queue_name.

How do I configure Hutch to use TLS with RabbitMQ?

The hutch command line runner takes -t or --[no-]mq-tls for the AMQP connection, along with --mq-tls-cert for the certificate and --mq-tls-key for the private key used in client verification. Broker host, port, exchange, vhost and username are all set with matching --mq- options.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. ruby-amqp/hutch on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ruby-amqp-hutch.svg)](https://hysenlabs.com/projects/ruby-amqp-hutch)

Was this useful?