Instead of answering that by comparing documentation or APIs, I decided to build something.
I created a simple banking application using LavinMQ and implemented the same messaging flow with two Node.js clients: amqplib and amqp-client.js. A producer publishes transactions to LavinMQ, and a worker consumes and processes them.
Then I started breaking things.
What happens if transaction processing fails temporarily? What if a transaction shouldn’t be retried? What if the same message is delivered twice? And what happens when the connection between the worker and LavinMQ disappears completely?
That exercise became a four-part blog series covering the successful transaction path, failure handling, connection recovery, and migration between the two clients.
One thing that became clear while building the demo was that reliability patterns don’t belong to a particular client library.
Retries, dead-lettering, acknowledgements, durable queues, persistent messages, and idempotent processing were concepts I could apply with either client. The APIs were different, but the reasoning behind those patterns remained the same.
The bigger difference appeared when I started testing connection failures.
With amqplib I had to implement the recovery logic myself: detect when the connection closed, reconnect, recreate the channel, and restore the consumer. With the high-level AMQPSession API in amqp-client.js much of that connection and subscription recovery is handled by the client.
That difference also made me rethink how I structured the application.
Rather than spreading client-specific operations throughout the producer and worker, I introduced a broker.js layer to handle communication with LavinMQ. The producer and worker could then interact with the broker through methods such as publishTransaction(), subscribe(), retry(), and acknowledge() without directly depending on the underlying client’s API.
This became particularly useful when I started the migration.
At first, switching from one AMQP client to another can sound like rewriting the application. But with the messaging operations separated from the banking logic, the migration was mostly about replacing the implementation inside broker.js.
The transaction-processing logic, retry strategy, dead-lettering, and idempotency remained the same. What changed was how the application established connections, published and consumed messages, acknowledged deliveries, and recovered from connection failures.
For me, that was probably the most useful lesson from the whole exercise.
Building a reliable messaging application isn’t just about choosing a client that supports the right features. It’s about understanding which responsibilities belong to LavinMQ, which belong to your application, and which you’re handing over to the client library.
And if you keep those responsibilities clearly separated, changing clients becomes much easier.
Curious to see how it works? Check out the four-part blog series and explore the demo on GitHub.
👋
This blog is written by developers at CloudAMQP to showcase CloudAMQP improvements. We have over a decade of experience helping companies expand their messaging knowledge and usage. Support is included in all CloudAMQP plans, available around the clock.