Skip to content

Connections & Channels

More production RabbitMQ incidents come from connection misuse than from anything else. A TCP connection with its TLS handshake and AMQP negotiation is expensive to open. Code that opens a connection per message — or per request — floods the broker with churn, exhausts file descriptors, and falls over under load.

The rule from the foundations module, restated for production:

One long-lived connection per process. Many channels over it — one per worker or concurrent task. Channels are not thread-safe.

flowchart LR
  app["App process"] --> conn["1 long-lived
connection"]
  conn --> ch1["Channel → worker 1"]
  conn --> ch2["Channel → worker 2"]
  conn --> ch3["Channel → worker 3"]
  ch1 --> broker["RabbitMQ"]
  ch2 --> broker
  ch3 --> broker
The correct shape: one connection, many channels, one channel per worker

A connection can look alive at the TCP level while the peer is actually gone (a crashed node, a silent network drop). Heartbeats are periodic frames both sides exchange; if several are missed, the connection is considered dead and torn down. This lets a client detect a broken broker in seconds instead of waiting for a TCP timeout that can take minutes. Keep the default heartbeat on — turning it off is how “zombie” connections happen.

Networks blip. A robust client does not crash when a connection drops — it reconnects, then re-declares its topology (queues, exchanges, bindings) and re-establishes its consumers. Most clients offer this, but you must design for it: a redelivered message may arrive after recovery, so your consumers still need to be idempotent.

import amqp from 'amqplib';
// amqplib has no built-in recovery — wrap connect with retry.
async function connectWithRetry(url, attempt = 0) {
try {
const conn = await amqp.connect(url, { heartbeat: 30 });
conn.on('error', (e) => console.error('conn error', e.message));
conn.on('close', () => setTimeout(() => connectWithRetry(url), 2000));
return conn;
} catch (e) {
const delay = Math.min(2 ** attempt * 500, 30000);
console.warn(`connect failed, retry in ${delay}ms`);
await new Promise((r) => setTimeout(r, delay));
return connectWithRetry(url, attempt + 1);
}
}
  • One connection per process, reused for the app’s lifetime.
  • One channel per worker/goroutine/thread — never share a channel across threads.
  • Separate the publisher channel from consumer channels (a channel error, like publishing to a missing exchange, closes the whole channel).
  • Keep heartbeats on so dead connections are detected quickly.
  • Reconnect with backoff and re-declare topology + consumers on recovery.
What is the most common production mistake with RabbitMQ connections?
What do heartbeats accomplish?
After an automatic reconnection, what must a robust client do?
Why keep publisher and consumer work on separate channels?