Python
Pika
Queue Processing
Programming
Data Consumption

Consume multiple queues in python / pika

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

With RabbitMQ and pika, one process can consume from multiple queues without opening a separate worker for each queue. The practical question is not whether this is possible, but whether the queues have similar enough behavior to share the same connection, channel, and flow-control settings.

Register Multiple Consumers on One Channel

With the blocking adapter, a single channel can call basic_consume more than once. RabbitMQ then delivers messages from whichever queue has work ready.

python
1import pika
2
3connection = pika.BlockingConnection(
4    pika.ConnectionParameters("localhost")
5)
6channel = connection.channel()
7channel.basic_qos(prefetch_count=1)
8
9
10def handle_orders(ch, method, properties, body):
11    print("order:", body.decode())
12    ch.basic_ack(delivery_tag=method.delivery_tag)
13
14
15def handle_emails(ch, method, properties, body):
16    print("email:", body.decode())
17    ch.basic_ack(delivery_tag=method.delivery_tag)
18
19
20channel.basic_consume(queue="orders", on_message_callback=handle_orders)
21channel.basic_consume(queue="emails", on_message_callback=handle_emails)
22
23try:
24    channel.start_consuming()
25finally:
26    connection.close()

That is often all you need. A single event loop is easy to operate and avoids unnecessary connection overhead.

One Callback Can Handle Several Queues Too

If the queues share the same processing logic, you can reuse one callback and branch based on queue metadata. method.routing_key or the consumer tag can help distinguish the source.

python
1import pika
2
3
4def handle_message(ch, method, properties, body):
5    queue_name = method.routing_key
6    print(f"from {queue_name}: {body.decode()}")
7    ch.basic_ack(delivery_tag=method.delivery_tag)
8
9
10connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
11channel = connection.channel()
12channel.basic_qos(prefetch_count=2)
13
14channel.basic_consume(queue="orders", on_message_callback=handle_message)
15channel.basic_consume(queue="emails", on_message_callback=handle_message)
16channel.start_consuming()

This keeps the setup compact when the only difference is routing rather than processing behavior.

Use Manual Acknowledgments and Sensible QoS

When several queues share one consumer, acknowledgment strategy matters more than queue count. Avoid auto_ack=True unless losing messages on consumer failure is acceptable.

python
1import json
2import pika
3
4
5def handle_json(ch, method, properties, body):
6    try:
7        payload = json.loads(body)
8        print(payload["event"])
9        ch.basic_ack(delivery_tag=method.delivery_tag)
10    except Exception:
11        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

basic_qos(prefetch_count=...) is also important. It limits how many unacknowledged deliveries the consumer can hold. Without it, one busy process can accumulate too much in-flight work before it has proved it can keep up.

Know When to Split Queues Apart

Sharing a consumer is a good default when the queues have similar latency and throughput expectations. It becomes a bad fit when one queue contains slow work and another contains latency-sensitive messages.

For example, if the orders queue triggers long-running image generation but emails needs fast turnaround, a single blocking consumer can create head-of-line blocking. In that case, separate workers are easier to tune and scale.

Split queues into separate workers when:

  • one queue needs a different prefetch count
  • one callback is much slower than the others
  • workloads need different deployment or retry policies
  • operations wants independent scaling per queue type

The simplest working architecture is usually best at first. Split only when the data shows interference.

Be Careful with Threads and Connections

A common but fragile design is to share one BlockingConnection across several Python threads. That is not the safest default. If you need real concurrency, prefer one connection per worker thread or move to an asynchronous adapter designed for that model.

For many systems, a practical progression is:

  1. Start with one process and one channel consuming several queues.
  2. Measure queue lag and callback duration.
  3. Break heavy or latency-sensitive queues into dedicated workers if needed.

That keeps the RabbitMQ topology understandable while leaving room to scale later.

Common Pitfalls

  • Creating one process per queue before there is evidence that isolation is needed.
  • Using auto_ack=True and losing work when a consumer crashes mid-processing.
  • Mixing very slow and very fast workloads on the same blocking consumer.
  • Ignoring basic_qos, which allows too much unacknowledged work to pile up.
  • Sharing one blocking connection carelessly across threads.

Summary

  • A single pika channel can consume from multiple queues by calling basic_consume multiple times.
  • Shared consumers work well when the queues have similar performance characteristics.
  • Manual acknowledgments and basic_qos matter more than raw queue count.
  • Split queues into separate workers only when you need different scaling or isolation.
  • Be conservative about threading with the blocking adapter.

Course illustration
Course illustration

All Rights Reserved.