Where REST starts to struggle
Most systems start with REST. One service calls another over HTTP, waits for the answer, and moves on. It’s simple and familiar, and it works well until traffic increases. Then the cracks show. A slow downstream service drags the caller down. A traffic spike turns into a wall of timeouts. A failed request means lost work, unless someone wrote retry logic by hand.
REST and message brokers solve different problems
A message broker fixes exactly these issues, but it doesn’t replace REST. Most mature architectures use both. Read on to discover what a broker gives you that REST can’t, when REST is still the better choice, and why the same code runs on both RabbitMQ and LavinMQ.
Sync vs async: a phone call vs a mailbox
When I talk about messaging at workshops, I usually compare it to a phone call and a mailbox.
REST is a phone call. I call you, I wait for you to pick up, and then we talk. That works fine as long as I’m the only one calling. But what happens if everyone in the room calls you at the same time? You can take one call. Everyone else gets a busy signal, waits on hold, or hangs up. That is exactly what a service under load looks like over REST: busy signals become timeouts, and people who hang up become lost requests.
After that, I usually compare a message broker to a mailbox. Everyone in the room can drop off a message at the same time and get on with their day. Someone else empties the mailbox when they are ready, and nothing gets lost.
How is it different from the mailbox at the end of your driveway? A message broker isn’t slow. The broker delivers messages in milliseconds, so you get the reliability of a mailbox with speed close to a phone call.
REST vs RabbitMQ: the fundamental difference
In technical terms, the phone call is synchronous. The caller sends a request and waits for the receiver to answer, so both services must be up, healthy, and fast at the same time. Engineers call this tight coupling, or more precisely, temporal coupling.
The mailbox is asynchronous. The producer publishes a message to an exchange, the broker routes it to one or more queues, and a consumer picks it up when it is ready. The producer doesn’t wait, and it doesn’t even need to know who the consumer is.
| REST (HTTP) | Message broker (AMQP) | |
|---|---|---|
| Communication | Synchronous request/response | Asynchronous publish/consume |
| Both sides online? | Yes, at the same time | No, the queue holds messages |
| Delivery guarantees | Up to you (retries, idempotency) | Built in (persistence, acks, redelivery) |
| One-to-many | One call per receiver | Fan-out via exchanges |
| Traffic spikes | Hit the receiver directly | Absorbed by the queue |
| Best for | Queries, CRUD, public APIs | Events, background jobs, service-to-service work |
Six advantages a message broker holds over REST
1. Services don’t have to be online at the same time
With REST, if the email service is down, the order service gets an error and has to decide what to do. With a broker, the order service publishes the order to a queue, where it waits. When the email service comes back, it processes the backlog. A deploy, a restart, or a crash no longer breaks the flow.
2. Queue-based load leveling: absorb traffic spikes instead of failing
A flash sale can multiply traffic in seconds. Over REST, every extra request hits the receiving service directly, which either scales instantly or starts failing. A queue absorbs the peak and lets consumers work through it at a steady pace. The queue acts as a buffer, so the receiving service only sees the load it can handle.
3. Reliability is built in, not bolted on
Making REST reliable means writing retries, backoff, idempotency keys, and somewhere to park failed requests. An AMQP broker ships with these building blocks:
- Persistent messages survive a broker restart.
- Consumer acknowledgements mean the broker only removes a message once the consumer has processed it.
- Redelivery hands unacknowledged messages to another consumer.
- Dead-letter exchanges collect messages that keep failing, so nothing disappears silently.
4. One event, many receivers
When a customer places an order, billing, shipping, analytics, and email may all need to be informed. Over REST, the order service calls each one, and someone has to update it every time you add a new receiver. With a fanout or topic exchange, the order service publishes once. New consumers simply bind a queue and start listening for updates.
5. Scaling is adding consumers
Is a queue growing faster than it drains? Start another consumer on the same queue. The broker distributes messages between them automatically, with no load balancer to configure.
6. Backpressure instead of timeouts
With the consumer prefetch setting, each consumer only takes as many messages as it can handle. Slow consumers get less work, fast ones get more, and nobody drowns. Over REST, the same situation shows up as timeouts and 503 errors.
When REST is still the right choice
A broker is not a replacement for every HTTP call. REST is still the better fit when the user needs an answer right away: fetching a product page or checking a balance is a question, not an event. It’s also the natural choice for public APIs, where third parties expect HTTP, JSON, and standard tooling, and for simple, low-volume CRUD that doesn’t need guaranteed delivery. And if your system is small and you don’t expect it to grow, the extra infrastructure may not be worth it.
In practice, the strongest pattern is a hybrid: REST at the edge, messaging behind it. The API accepts the request, validates it, publishes a message, and returns 202 Accepted right away. The heavy lifting happens asynchronously, and the user never waits for it.
The same flow, two ways
Take a common case: a customer places an order, and the system sends a confirmation email.
With REST, the order service calls the email service directly and has to handle every failure on its own:
import requests
def place_order(order):
save_order(order)
try:
requests.post("http://email-service/send", json=order, timeout=2)
except requests.RequestException:
# Email service down or slow: retry? store it? give up?
log_failure(order)
With a message broker, the order service publishes an event and moves on:
import json, os, pika
connection = pika.BlockingConnection(pika.URLParameters(os.environ["AMQP_URL"]))
channel = connection.channel()
channel.queue_declare(queue="order.created", durable=True)
def place_order(order):
save_order(order)
channel.basic_publish(
exchange="",
routing_key="order.created",
body=json.dumps(order),
properties=pika.BasicProperties(delivery_mode=2), # persistent
)
The email service consumes at its own pace and acknowledges each message only after it sends the email:
def on_message(ch, method, properties, body):
send_email(json.loads(body))
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_qos(prefetch_count=10)
channel.basic_consume(queue="order.created", on_message_callback=on_message)
channel.start_consuming()
If the email service crashes mid-send, the broker redelivers the unacknowledged message. No custom retry logic needed.
This code uses pika, the standard Python client for AMQP 0-9-1. Point AMQP_URL at RabbitMQ or LavinMQ and it runs unchanged.
How to start: moving from REST to async in four steps
You don’t need to rewrite your system to get the benefits of a message broker. Start with one flow and grow from there.
1. Find a call that doesn’t need an immediate answer
Look for work the user doesn’t have to wait for: confirmation emails, PDF generation, image processing, notifications, or syncing data to other systems. Slow endpoints and timeouts in your logs are good places to start.
2. Publish a message instead of calling the service
Keep your REST endpoint, so clients still talk to the same API. But instead of calling the downstream service and waiting, the endpoint validates the request, publishes a message to the broker, and returns 202 Accepted right away.
3. Move the work into a consumer
The downstream service reads messages from the queue, does the job, and acknowledges each message when it’s done. Turn on persistent messages and a dead-letter exchange from the start, so you don’t lose messages when something fails.
4. Scale by adding consumers
Keep an eye on queue depth. A queue that keeps growing is your signal to scale: start another consumer on the same queue, and the broker shares the work between them. No load balancer, no changes to the producer.
Once the first flow works, repeat it for the next one. Over time, REST stays at the edge while the heavy work runs asynchronously behind it. That’s the hybrid pattern described above, and it’s how most systems outgrow REST without a big rewrite.
Choosing a broker: RabbitMQ or LavinMQ
RabbitMQ made AMQP messaging mainstream, and everything above applies to it. But it isn’t the only broker that speaks the protocol.
LavinMQ is an open-source message broker built by the team behind CloudAMQP, which has run RabbitMQ at scale for over a decade. LavinMQ implements AMQP 0-9-1, so the exchanges, queues, bindings, acks, and client libraries you already know work the same way.
LavinMQ delivers high throughput on modest hardware (see the benchmark results) and keeps memory use low. It’s simple to run, a single binary with no runtime dependencies, and supports streams and MQTT alongside classic AMQP queues.
If you already run RabbitMQ and it works for you, great. If you are starting fresh, want lower infrastructure costs, or need to handle deep queues without memory pressure, LavinMQ is worth a test run.
FAQ
Can RabbitMQ replace REST?
Not entirely. REST is still the natural choice for queries that need an immediate answer and for public APIs. Most systems use both.
Is a message queue faster than REST?
For the caller, usually yes: publishing a message returns in milliseconds, while a REST call waits for the whole job to finish. End-to-end processing isn’t necessarily faster, but the system as a whole handles load better.
Can I use RabbitMQ client libraries with LavinMQ?
Yes. LavinMQ is compatible with AMQP 0-9-1, so standard RabbitMQ clients such as pika (Python), amqplib (Node.js), the RabbitMQ Java client, and Bunny (Ruby) connect without code changes.
When should I not use a message queue?
When the caller needs the result right away, when the system is small and you don’t expect it to grow, or when the added infrastructure costs more than the reliability it brings.
Try message brokers yourself
The fastest way to feel the difference is to run the example above. Start LavinMQ locally with Docker:
docker run -p 5672:5672 -p 15672:15672 cloudamqp/lavinmq
Then set AMQP_URL=amqp://guest:guest@localhost and run the producer and consumer. Prefer not to run it yourself? Create a free hosted LavinMQ instance and have a broker running in under a minute.
About the author
Lovisa Johansson works at CloudAMQP, the team behind the message broker LavinMQ, and has worked with hosted RabbitMQ for many years. She runs workshops on messaging and event-driven architecture and is the author of RabbitMQ Essentials and LavinMQ: Your Fast Track to Lean Messaging.