RabbitMQ Messaging skill for Codex
Confirmed publishes, acked work, no poison loops. Codex lays out exchanges, routing keys and one quorum queue per consumer service, publishes with confirms and mandatory routing so nothing vanishes, and consumes with manual acks, bounded prefetch and messageId deduplication. Every queue gets a dead-letter policy and delivery limit, transient errors go through TTL wait queues, and alerts watch dead queues and consumerless backlogs.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- rabbitmq-messaging Installs to ~/.agents/skills/rabbitmq-messaging/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install rabbitmq-messaging The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project rabbitmq-messaging Installs to .agents/skills/rabbitmq-messaging/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/rabbitmq-messaging.zipunzip -o rabbitmq-messaging.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) rabbitmq-messaging PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $rabbitmq-messaging, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs RabbitMQ messaging for services - exchange and routing-key topology, quorum queues versus streams, publisher confirms and mandatory routing, manual acks and prefetch, dead-letter exchanges and delayed retries, poison-message limits, connection and channel handling, and broker monitoring with rabbitmqctl and Prometheus. Use when adding a producer or consumer, messages are lost, duplicated or stuck unacked, queues grow without bound, consumers loop on a bad message, or when migrating off classic mirrored queues.
Use it when
- Adding a RabbitMQ producer, consumer or exchange
- Messages are lost, duplicated or stuck unacknowledged
- A poison message loops or a queue grows without bound
- Upgrading to RabbitMQ 4 or moving to quorum queues
Use something else when
- Simple in-app background work with a job library: use background-jobs
- Event streaming with long retention and partitions: use kafka-streaming
How to check the result
Integration tests show a confirmed publish consumed once, an unroutable message returned, and a permanently failing message dead-lettered exactly once without looping.
The mistake it prevents
Calling nack with requeue=true on errors that will recur, creating a hot redelivery loop that pins CPU and starves every other message in the queue.
A RabbitMQ integration is sound when publishers know the broker accepted and routed each message, consumers acknowledge only after the work is durable, every queue has a dead-letter path and a redelivery limit, and the topology is declared in code or definitions rather than clicked together in the UI.
Workflow
- [ ] 1. Inspect broker version, topology and client libraries
- [ ] 2. Design exchanges, routing keys and queues
- [ ] 3. Publish with confirms and mandatory routing
- [ ] 4. Consume with manual acks, prefetch and idempotency
- [ ] 5. Add dead-lettering, delayed retry and limits
- [ ] 6. Manage connections, then monitor
1. Inspect
- Read
AGENTS.md, then find connection setup, topology declarations (assertExchange,queue_declare,definitions.json) and the broker version (rabbitmq-diagnostics statusor the management UI). - RabbitMQ 4.x removed classic mirrored queues. Any
ha-modepolicy is a migration item to quorum queues. - List existing queues and their state if you have access:
rabbitmqctl list_queues name type messages_ready messages_unacknowledged consumers
rabbitmqctl list_exchanges name type
rabbitmqctl list_bindings source_name destination_name routing_key
These commands need access to the broker host or container; in a sandbox without it, ask the user to run them and paste the output.
2. Topology
| Need | Exchange | Routing |
|---|---|---|
| Events many services may subscribe to | topic (e.g. orders) | order.created, order.cancelled.eu |
| Commands to exactly one handler type | direct | queue name or command name |
| Broadcast to every bound queue regardless of key | fanout | ignored |
- One exchange per domain or bounded context; one queue per consumer service per
purpose (
billing.order-created). Never let two different services share a queue unless they are replicas of the same consumer. - Routing keys are
entity.event[.qualifier]. Bind with*(one word) or#(zero or more) deliberately;#bindings silently receive every future event.
| Queue type | Use for |
|---|---|
Quorum (x-queue-type: quorum) | Default for durable work queues; replicated, poison-message limit built in |
Stream (x-queue-type: stream) | Replay, large fan-out, many consumers reading the same history |
| Classic, non-durable or exclusive | Temporary reply queues, transient per-connection data |
Queue type and other arguments are fixed at declaration; redeclaring with different
arguments fails with PRECONDITION_FAILED. Changing them means a new queue and a
migration. Tunables that can change (DLX, delivery limit, TTL, max length) belong in
policies.
3. Publish reliably
import amqp from 'amqplib';
const conn = await amqp.connect(process.env.AMQP_URL!);
const ch = await conn.createConfirmChannel();
await ch.assertExchange('orders', 'topic', { durable: true });
ch.on('return', (msg) => {
log.error({ routingKey: msg.fields.routingKey }, 'unroutable message returned');
});
export async function publishOrderCreated(order: Order) {
ch.publish('orders', 'order.created', Buffer.from(JSON.stringify({ orderId: order.id })), {
persistent: true,
mandatory: true, // return it if no queue is bound
messageId: `order.created:${order.id}`,
contentType: 'application/json',
timestamp: Math.floor(Date.now() / 1000),
headers: { 'schema-version': '1' },
});
await ch.waitForConfirms(); // broker has taken responsibility
}
- Publisher confirms are the only way to know the broker accepted a message. Batch publishes and wait once per batch for throughput.
mandatory: trueplus areturnhandler (or an alternate exchange) catches messages that matched no binding; otherwise they are silently dropped.- Publishing after a database commit can still lose the message if the process dies in between; write to an outbox in the same transaction and relay (see outbox-pattern).
- Set a stable
messageIdso consumers can deduplicate.
4. Consume safely
const ch = await conn.createChannel();
await ch.assertQueue('billing.order-created', {
durable: true,
arguments: { 'x-queue-type': 'quorum' },
});
await ch.bindQueue('billing.order-created', 'orders', 'order.created');
await ch.prefetch(20);
await ch.consume('billing.order-created', async (msg) => {
if (!msg) return; // consumer cancelled by broker
try {
const body = JSON.parse(msg.content.toString());
await handleOnce(msg.properties.messageId, () => createInvoice(body.orderId));
ch.ack(msg);
} catch (err) {
if (isTransient(err)) return scheduleRetry(ch, msg); // see step 5
ch.nack(msg, false, false); // to the dead-letter exchange
}
}, { noAck: false });
- Manual acks only. Ack after the side effect is committed, never before.
- Prefetch bounds unacked messages per consumer: 10-50 for typical I/O-bound handlers, 1-5 for slow or heavy ones. Unlimited prefetch makes one consumer hoard the queue.
- Delivery is at-least-once: dedupe on
messageIdin the same transaction as the effect (an inbox table with a unique key). - Ordering holds only per queue with one consumer and no requeues. If order matters,
use
x-single-active-consumer: trueor a version check in the handler.
5. Dead letters, retries, poison messages
rabbitmqctl set_policy --apply-to queues billing-dlx "^billing\." \
'{"dead-letter-exchange":"billing.dlx","delivery-limit":5}'
- Every work queue gets a dead-letter exchange and a bound
*.deadqueue that someone monitors. Quorum queues in 4.x default to a delivery limit of 20; set it explicitly. - Never
nackwithrequeue=trueon errors that will repeat; that is a hot loop. - Delayed retry pattern: on transient failure, republish to a retry exchange with a
per-delay wait queue (
x-message-ttl5 s, 30 s, 5 min) whose dead-letter exchange routes back to the work exchange; increment your ownx-retry-countheader and ack the original. After N retries, publish to the dead queue instead. - For quorum queues that must not lose dead-lettered messages, use
dead-letter-strategy: at-least-once, which requiresoverflow: reject-publish. - Cap queue growth with
max-lengthormax-length-bytespolicies plus alerts; unbounded queues eventually trigger memory or disk alarms that block all publishers.
6. Connections and monitoring
- One long-lived connection per process (or one for publishing and one for consuming); channels per thread or task. Never open a connection per message.
- Channels are not thread-safe in most clients; do not share one across threads.
- Handle reconnects: Java and .NET clients and aio-pika’s
connect_robustrecover automatically; with amqplib or pika you must re-create connection, channels and consumers onclose/error. - Heartbeats on (default 60 s negotiation) so dead TCP connections are detected.
- Monitor via the Prometheus plugin (port 15692) or management API: messages ready, unacked, consumer count per queue, publish vs deliver rates, memory and disk alarms, dead-letter queue depth. Alert on “ready messages growing with zero consumers” and on any dead queue depth above zero.
Gotchas
- Declaring a queue from both producer and consumer with different arguments causes
PRECONDITION_FAILEDon whichever starts second. - Auto-ack (
noAck: true) loses every in-flight message when a consumer crashes. - A slow handler with a large prefetch holds hundreds of messages invisible to other consumers, while the queue looks idle.
- Long-running handlers exceeding the consumer acknowledgement timeout (30 minutes by default) get their channel closed and messages redelivered.
- Purging or deleting queues destroys messages permanently; confirm with the user before
running
rabbitmqctl purge_queueor deleting anything in production. - Using RabbitMQ as a database (millions of messages parked for days) degrades the broker; store state elsewhere and keep queues short.
Verification
- Integration test against a disposable broker (
docker run -p 5672:5672 rabbitmq:4-management, needs Docker and an image pull, or testcontainers): publish with confirms, consume, assert one effect. - Publish to a routing key with no binding and confirm the return handler fires.
- Throw a permanent error in the handler: message lands in the dead queue once, no loop.
- Kill the consumer mid-message: message is redelivered and processed once in effect.
rabbitmqctl list_queuesshows the expected type, consumers and zero unacked at rest.
Output template
## Messaging change
- Exchange(s): <name, type>
- Routing keys: <keys>
- Queues: <name, type, bindings, consumers>
- Policies: <DLX, delivery-limit, max-length>
- Publisher: confirms <yes>, mandatory <yes>, outbox <yes/no>
- Consumer: prefetch <n>, dedupe on <messageId>, retry schedule <delays>
- Alerts: <metrics and thresholds> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "RabbitMQ Messaging"
short_description: "Confirmed publishes, acked work, no poison loops"
default_prompt: "$rabbitmq-messaging Add a RabbitMQ consumer for order.created events in the billing service" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.