Send your first message with pika
Send your first message with pika — Messaging & queues.
Focus: send your first message with pika
You've built services that talk to each other over HTTP, but now you need something that can handle a spike of 10,000 orders without your API falling over — and you need each order processed exactly once, in order. That's the pain point this lesson solves: sending your first message with pika, the Python client for RabbitMQ, so you can decouple producers from consumers, buffer spikes, and build systems that actually scale. No fluff, no theory — you're going to write code that puts a message on a queue and pull it off on the other side.
The problem this lesson solves
Web requests are synchronous: a client sends a request and waits for a response. That's fine for simple CRUD, but it breaks under bursty load or when a job takes minutes (like resizing a video or generating a PDF report). Your HTTP server blocks, timeouts happen, and users see 503 errors.
Messaging solves this by decoupling the producer (who sends the work) from the consumer (who does the work). Instead of calling a service directly, you drop a message onto a queue, and a consumer picks it up whenever it's ready. The producer doesn't wait; the consumer doesn't need to be online at the same time.
But here's the gotcha: you can't just "send a message" to a queue without a client library. RabbitMQ speaks AMQP 0-9-1, and pika is the battle-tested Python client that translates your Python objects into bytes and back. Without it, you'd be hand-crafting AMQP frames over TCP — nobody wants that.
This lesson is step 3 in the Messaging & queues track. You've already learned the queue-vs-log metaphor and why idempotency matters; now you'll make it real by sending your first message with pika.
Core concept / mental model
Think of RabbitMQ as a post office. You (the producer) write a letter (the message), put it in an envelope (the AMQP frame), and drop it in a mailbox (the exchange). The post office sorts letters and delivers them to specific post office boxes (queues) based on routing rules. A consumer picks up the mail whenever they check their box.
The key components:
- Producers — applications that publish messages.
- Consumers — applications that receive and process messages.
- Exchanges — the smart part; they receive messages and route them to queues based on bindings and routing keys.
- Queues — the actual buffers that store messages until a consumer takes them.
- Bindings — rules that connect an exchange to a queue.
- AMQP — the protocol that defines all this machinery.
Think of an exchange as a router table — it doesn't store anything; it just decides which queue(s) get the message based on the routing key you provide.
For your first message, you'll use a direct exchange (the default) and a queue named hello. The exchange is '', the default exchange, which routes messages directly to a queue by its name — as if you addressed the envelope to a specific post office box.
How it works step by step
Sending a message with pika follows a predictable sequence:
- Connect to the RabbitMQ broker using
BlockingConnection(synchronous, great for scripts). - Create a channel — a lightweight conversation over the connection; all operations happen on a channel.
- Declare a queue — ensure the queue exists (idempotent: declaring
hellowhen it doesn't exist creates it; when it does, it's a no-op). - Publish a message to the default exchange with
basic_publish, specifying the routing key as the queue name. - Close the connection cleanly to flush buffers and avoid leaks.
The magic is that basic_publish serializes your message to bytes automatically. If you pass a string, pika encodes it as UTF-8. If you pass bytes, it sends them as-is. You can even pass a dict with json.dumps() to send structured data.
Here's the cause-and-effect chain: you connect → channel → queue declaration → publish → close. If you declare a queue that doesn't exist, RabbitMQ creates it. If a consumer is listening, it gets the message almost instantly. If not, the message sits in the queue until someone reads it — that's the durability you get for free.
Hands-on walkthrough
Prerequisites
Make sure RabbitMQ is running locally (e.g., via Docker):
docker run -d --name rabbitmq -p 5672:5672 rabbitmq:3
And install pika:
pip install pika
Send your first message
Create a file send.py:
import pika
# 1. Connect to RabbitMQ on localhost:5672
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# 2. Declare a queue named 'hello'
channel.queue_declare(queue='hello')
# 3. Publish a message to the default exchange, routing key 'hello'
channel.basic_publish(exchange='', routing_key='hello', body='Hello, world!')
print("[x] Sent 'Hello, world!'")
# 4. Close the connection to flush and release resources
connection.close()
Run it:
python send.py
Expected output:
[x] Sent 'Hello, world!'
That's it — you've sent your first message with pika. But hold on: how do you know it actually arrived? Let's write a consumer.
Receive the message
Create receive.py:
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# Ensure the queue exists (same as in send.py)
channel.queue_declare(queue='hello')
# Callback that runs when a message is received
def callback(ch, method, properties, body):
print(f"[x] Received {body.decode()}")
# Tell RabbitMQ to deliver messages from 'hello' to this callback
channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=True)
print('[*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
Run the consumer in one terminal:
python receive.py
Then run the producer in another terminal:
python send.py
You'll see on the consumer side:
[*] Waiting for messages. To exit press CTRL+C
[x] Received Hello, world!
Notice the flow: the consumer declares the same queue, so it's guaranteed to exist. If you run the producer first, the message sits in the queue until the consumer starts — that's your buffer.
Sending structured data
For real applications, you'll send JSON:
import json
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='orders')
order = {"order_id": 101, "product": "Acme Widget", "qty": 3}
channel.basic_publish(
exchange='',
routing_key='orders',
body=json.dumps(order),
properties=pika.BasicProperties(content_type='application/json')
)
print("[x] Sent order", order)
connection.close()
The consumer on the other side does json.loads(body) to reconstruct the dict. This is the pattern you'll use in production — message bodies are almost always JSON or Avro.
Pro tip: Always
close()the connection when you're done. A pika connection holds a TCP socket; leaving it open in a long-running script wastes resources and can cause "connection reset by peer" errors when the broker restarts.
Compare options / when to choose what
Pika is the most popular client, but it's not the only one. Here's a quick comparison:
| Client | Pros | Cons | Best for |
|---|---|---|---|
| Pika | Synchronous, easy to learn, pure Python, first-class support for AMQP 0-9-1 | Blocking connection can't handle multiple channels in threads easily | Scripts, tutorials, small to medium apps |
| aio-pika | Async, integrates with asyncio, performant | Requires async thinking, steeper learning curve | High-concurrency async services |
| Kombu | Higher-level abstraction, supports RabbitMQ and Redis, integrates with Celery | Heavier dependency, hides AMQP details | Celery tasks, multi-broker support |
When to choose pika: you're writing a simple producer/consumer script, a cron job, or a microservice that doesn't need async I/O. It's the fastest way to get a message on the wire and debug your pipeline.
When to avoid pika: you're building a high-throughput, non-blocking service where asyncio is a requirement — then aio-pika is a better fit. If you need a task queue with retries and scheduling, Celery (built on Kombu) is the way to go.
Troubleshooting & edge cases
Connection refused
If you get pika.exceptions.AMQPConnectionError or Connection refused, RabbitMQ isn't running or is on a different host/port. Check:
# Is the container running?
docker ps
docker logs rabbitmq
# Is port 5672 open?
ss -tlnp | grep 5672
If you're using Docker, ensure you mapped -p 5672:5672.
Queue declaration mismatch
If your producer declares hello and your consumer declares hello2, the consumer will never see messages — the consumer listens on a different queue. Keep the queue name consistent across both sides. This is a classic typo bug.
Messages lost when consumer is down
By default, RabbitMQ's queue is not durable and messages are not persistent. If the broker restarts, the queue and its messages vanish. For critical messages, set durable=True on the queue and delivery_mode=2 on the message:
channel.queue_declare(queue='hello', durable=True)
channel.basic_publish(
exchange='',
routing_key='hello',
body='Hello',
properties=pika.BasicProperties(delivery_mode=2) # persistent message
)
auto_ack=True and message loss
If your consumer crashes mid-processing with auto_ack=True, the message is lost forever — RabbitMQ assumes it was handled. In production, use auto_ack=False and manually basic_ack after processing. We'll dive into this in the idempotency lesson.
"PRECONDITION_FAILED" error
If you declare a queue with different parameters (e.g., durable=False after previously declaring it durable=True), RabbitMQ raises channel.queue_declare PRECONDITION_FAILED. You must either delete the queue or keep the parameters consistent across declarations.
What you learned & what's next
Let's recap what you've accomplished:
- You connected to RabbitMQ using pika's
BlockingConnection. - You declared a queue idempotently with
queue_declare. - You published your first message with
basic_publishto the default exchange. - You consumed that message with a callback and
basic_consume. - You understood the roles of exchanges, queues, and bindings.
- You learned how to send structured JSON for real-world use.
These are the foundational skills for every queuing system you'll build. Now you can move to the next lesson: durable messaging and acknowledgements, where you'll learn to guarantee no message is lost even if the consumer crashes. You'll also explore how to scale consumers for throughput and handle dead-letter queues.
You're no longer just HTTP — you're asynchronous, resilient, and ready to handle real load. Keep going!
Practice recap
For a quick exercise, create a script that sends a JSON message with your name and favorite programming language to a queue named intro, then create a consumer that prints the decoded JSON. Try running the consumer after the producer to verify the message waits in the queue. Then, experiment with declaring the queue with durable=True and see what happens when you restart RabbitMQ.
Common mistakes
- Forgetting to close the connection — leaves TCP sockets open and can cause
ConnectionResetErroron broker restarts. - Declaring queues with different parameters (e.g.,
durable=Falsevsdurable=True) across producer and consumer, causing PRECONDITION_FAILED errors. - Using
auto_ack=Truein production without manual acknowledgment — messages are lost if your consumer crashes mid-processing. - Publishing to a queue without declaring it first — works with the default exchange, but fails with custom exchanges if the queue doesn't exist.
Variations
- Use
aio-pikafor asynchronous, asyncio-based producers/consumers that can handle many concurrent connections. - Use
Kombuwhen you need a higher-level abstraction or want to switch between RabbitMQ and Redis for your queue backend. - Send messages as bytes or JSON strings — pika accepts any bytes-like object;
json.dumps()pluspika.BasicProperties(content_type='application/json')is the standard pattern.
Real-world use cases
- An e-commerce checkout service publishes order events to a queue; an inventory service consumes them to update stock asynchronously.
- A video processing pipeline sends upload notifications to a queue; worker nodes pick up jobs to transcode videos without blocking the API.
- A monitoring system collects application logs and publishes them to a RabbitMQ queue; a log aggregator consumes and indexes them into Elasticsearch.
Key takeaways
- pika's
BlockingConnectionandchannel.basic_publishare the simplest way to send a message to RabbitMQ. - A queue declaration is idempotent — you can and should declare the same queue on both producer and consumer sides.
- The default exchange routes messages directly to a queue by its name, which works perfectly for simple 'post office box' scenarios.
- Always close your connection to release resources and flush messages.
- For durable messaging, set
queue_declare(durable=True)anddelivery_mode=2on published messages. - Use
auto_ack=Falseand manualbasic_ackin production to prevent message loss during consumer crashes.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.