Channels — Multiplexing over One Connection
Table of Contents
Channels — Multiplexing over One Connection #
A channel is a virtual session within an AMQP connection. Every AMQP operation — declaring a queue, publishing a message, consuming, acknowledging — happens on a channel. Multiple channels share a single TCP connection, amortizing connection overhead across many parallel logical sessions.
Why Channels Exist #
AMQP predates HTTP/2’s stream multiplexing but solves the same problem: one TCP connection is expensive to establish (handshake, TLS, authentication), but multiple parallel operations should not require multiple connections. Channels are the AMQP answer to this.
Connection (1 TCP socket, 1 OS file descriptor)
├── Channel 1 → producer publishing to exchange 'orders'
├── Channel 2 → consumer on queue 'order-processing'
├── Channel 3 → consumer on queue 'order-notifications'
└── Channel 4 → admin thread: declaring queues, checking counts
Each channel has an independent state machine: its own sequence of methods, its own flow control state, its own consumer list. A channel error on channel 2 does not affect channel 1 or channel 3.
Channel Numbering #
Channels are identified by a 16-bit unsigned integer (1–65535). Channel 0 is reserved for connection-level operations. The maximum channel number is bounded by the channel-max value negotiated in connection.tune.
The client chooses the channel number when opening a channel. By convention, channels are numbered sequentially starting from 1, but any unused number in range is valid.
Channel Lifecycle #
Opening a channel:
Client → Broker: channel.open (channel_id=1)
Broker → Client: channel.open-ok
Once open, a channel is ready for use. There is no authentication or vhost selection at the channel level — those happened at the connection level.
Using a channel:
- Declare exchanges, queues, bindings
- Publish messages
- Subscribe as a consumer (basic.consume)
- Acknowledge messages
Closing a channel gracefully:
Client → Broker: channel.close (reply-code=200, reply-text='Normal close')
Broker → Client: channel.close-ok
After channel.close-ok, the channel number can be reused. Any in-flight messages that were being delivered on the channel are returned to the queue.
Broker-initiated channel close (channel-level error):
Broker → Client: channel.close (reply-code=406, reply-text='PRECONDITION_FAILED...')
Client → Broker: channel.close-ok
When the broker detects a channel-level error (wrong routing key type, precondition failed on queue declare, etc.), it closes the channel and sends channel.close with an error code. The client must send channel.close-ok in response. The TCP connection and other channels remain open.
Channel Error Codes #
Channel-level errors close only the channel:
| Reply code | Name | Common cause |
|---|---|---|
| 311 | CONTENT_TOO_LARGE | Message body exceeds max |
| 312 | NO_ROUTE | Message published to exchange with no matching binding (mandatory flag) |
| 313 | NO_CONSUMERS | basic.get on a queue with no consumers (immediate flag) |
| 403 | ACCESS_REFUSED | Channel lacks permission for the operation |
| 404 | NOT_FOUND | Exchange or queue does not exist |
| 405 | RESOURCE_LOCKED | Queue is locked by another connection (exclusive queue) |
| 406 | PRECONDITION_FAILED | Redeclare with different parameters; passive declare of non-existent resource |
404 NOT_FOUND is the most commonly encountered channel error. If a consumer subscribes to a queue that does not exist, the broker closes the channel with 404. The fix is to declare the queue before subscribing, or declare it with passive=True to check existence without creation.
406 PRECONDITION_FAILED occurs when redeclaring an exchange or queue with different parameters. AMQP is idempotent: declaring an entity that already exists is allowed as long as the parameters match. If they differ, it is an error:
# First declare: durable=True
channel.queue_declare('my-queue', durable=True)
# Second declare (different connection): durable=False → PRECONDITION_FAILED
channel.queue_declare('my-queue', durable=False) # → channel.close (406)
Connection-Level Errors #
Some errors are severe enough to close the entire connection. These are connection-level errors sent as connection.close:
| Reply code | Name | Cause |
|---|---|---|
| 501 | FRAME_ERROR | Malformed frame (bad frame type, bad frame-end) |
| 502 | SYNTAX_ERROR | Method arguments cannot be parsed |
| 503 | COMMAND_INVALID | Method sent in wrong state (e.g., channel.open on channel 0) |
| 505 | UNEXPECTED_FRAME | Frame type unexpected for current state |
| 506 | RESOURCE_ERROR | Out of memory or disk on broker |
| 530 | NOT_ALLOWED | Attempt to use an operation not permitted (e.g., second connection.open) |
| 540 | NOT_IMPLEMENTED | Method not supported by this broker |
| 541 | INTERNAL_ERROR | Unrecoverable broker error |
Connection-level errors close all channels and the TCP connection. The client must reconnect from scratch.
Channel States #
A channel transitions through these states:
[not open]
↓ channel.open
[opening]
↓ channel.open-ok
[open] ←─── normal operation
↓ channel.close (or broker channel.close on error)
[closing]
↓ channel.close-ok
[closed]
In the open state, all operations are valid. In the closing state (after sending channel.close but before receiving channel.close-ok), the client should not send any more method frames on this channel except channel.close-ok in response to an unsolicited broker channel.close.
A channel that receives a broker-initiated channel.close must respond with channel.close-ok — even if the client was simultaneously trying to close it. This is the simultaneous close race: both sides may send channel.close at the same time. The AMQP specification handles this by having the client simply respond with channel.close-ok on receiving a broker-initiated close, regardless of the client’s own close state.
One Channel Per Thread #
The rule in AMQP client libraries: do not share channels across threads. The AMQP channel is a stateful object — method sequences must not be interleaved. If two threads concurrently send methods on the same channel, the resulting byte stream on the TCP connection will be garbled:
Thread 1 sends: basic.publish header
Thread 2 sends: basic.ack
Thread 1 sends: basic.publish body ← broker receives this after an ack, which is wrong
The correct model:
- One connection per process.
- One channel per thread (or per coroutine in async models).
- If needed, a pool of channels allocated per thread or task.
Async client model: in event-loop frameworks (asyncio, Tornado), a single event loop thread uses multiple channels. This is safe because only one frame is processed at a time. Libraries like aio-pika model this as:
async with connection.channel() as channel:
await channel.declare_queue('tasks')
await channel.basic_publish(exchange='', routing_key='tasks', body=b'work')
Flow Control #
AMQP 0-9-1 includes a channel-level flow control mechanism: channel.flow. The broker can send channel.flow(active=False) to pause a producer; channel.flow(active=True) to resume it.
Broker → Client: channel.flow (active=False)
← producer pauses publishing →
Broker → Client: channel.flow (active=True)
← producer resumes →
In practice, RabbitMQ deprecated channel.flow in favor of connection-level connection.blocked (Chapter 2) and TCP backpressure. Modern clients implement connection.blocked for backpressure, not channel.flow. However, channel.flow is part of the AMQP 0-9-1 spec and compliant brokers must support it.
Prefetch and Consumer-Specific State #
A channel that is in consumer mode (after basic.consume) has additional state:
- Prefetch count: the maximum number of unacknowledged messages the broker will deliver to this channel/consumer (set via
basic.qos). Covered in Chapter 11. - Consumer tags: each
basic.consumecall returns a consumer tag — a unique string identifying the consumer subscription on this channel. A channel can have multiple consumer tags (multiple subscriptions to different queues). - Delivery tag: an incrementing integer per channel identifying each delivered message, used in
basic.ack,basic.nack, andbasic.reject.
# Consumer tag returned by basic.consume
consumer_tag = channel.basic_consume(
queue='tasks',
on_message_callback=callback
)
# consumer_tag is a string like 'ctag1.abc123...'
The delivery tag and consumer tag are channel-local — they reset if the channel is closed and a new one opened.
Transactions on Channels #
AMQP 0-9-1 has channel-level transactions: tx.select, tx.commit, tx.rollback. These are not database transactions — they batch publishes and acks within a channel and commit or roll back together.
channel.tx_select() # enter transaction mode
channel.basic_publish(exchange='', routing_key='q', body=b'msg1')
channel.basic_publish(exchange='', routing_key='q', body=b'msg2')
channel.tx_commit() # atomically publish both messages
tx.rollback() discards all uncommitted publishes and acks.
Why transactions are rarely used: AMQP transactions are synchronous and slow — each tx.commit is a round-trip. Publisher confirms (Chapter 10) are the modern replacement: asynchronous, higher throughput, and can provide the same durability guarantee. RabbitMQ documentation explicitly recommends publisher confirms over transactions for most use cases.
Summary #
Channels are lightweight virtual sessions multiplexed over a single TCP connection. Each channel has an independent state machine, its own consumers, and its own delivery tag sequence. Channel errors close only the channel; connection errors close everything. The rule of one-channel-per-thread prevents frame interleaving. Channels carry all application-level operations; the connection (channel 0) carries only connection management. Prefetch, consumer tags, and transactions are channel-level state covered in later chapters.