Manual backup and restore for RabbitMQ
Platform backup was removed from RabbitMQ on 15 September 2026, so protecting a cluster's configuration is now something you run yourself. This page exports the cluster's definitions through the Management HTTP API, checks the file is complete, and imports it into a new cluster.
This backs up definitions only — vhosts, users, permissions, exchanges, queues, bindings, and policies. Messages sitting in queues are not included and cannot be recovered this way.
Applies to RabbitMQ 3.13.x on Erlang 26.x.
What definitions contain
| Component | What it covers |
|---|---|
| Vhosts | The virtual hosts |
| Users and permissions | Accounts and their access rights |
| Exchanges | Direct, topic, fanout, and headers exchanges |
| Queues | Queue declarations and their arguments |
| Bindings | The links between exchanges and queues |
| Policies | TTL, HA, max-length, dead-letter, and the rest |
How it works
Run everything from a backup server — a VM you control that can reach the RabbitMQ cluster on port 15672. It calls the Management HTTP API directly, so nothing is installed on the cluster itself and no node is ever stopped.
| Phase | What happens |
|---|---|
| Backup | The backup server calls GET /api/definitions on the source cluster and saves the JSON |
| Restore | The backup server posts a saved file to POST /api/definitions on the target cluster |
During a restore the source cluster is not contacted at all.
Prerequisites
| Item | Requirement |
|---|---|
| Network | The backup server reaches the cluster on port 15672 |
| Account | A user with the administrator tag |
| Target version | On restore, the target cluster's RabbitMQ version must be at or above the source |
| Erlang version | Should match the source cluster |
The commands below use these placeholders: <HOST> is the source cluster address and <TARGET_HOST> the destination one, <USERNAME> and <PASSWORD> are the administrator credentials, <VHOST_NAME> is a vhost name, <TIMESTAMP> is the stamp in a saved filename, and <RABBITMQ_HOST> is the address the scheduled script reads.
Back up the definitions
Export the whole cluster
curl -s \
-u '<USERNAME>:<PASSWORD>' \
http://<HOST>:15672/api/definitions \
-o /backup/rabbitmq/rabbitmq_definitions_$(date +%Y%m%d_%H%M%S).json
Example:
curl -s \
-u 'admin:your_password' \
http://10.0.1.10:15672/api/definitions \
-o /backup/rabbitmq/rabbitmq_definitions_$(date +%Y%m%d_%H%M%S).json
Export a single vhost
curl -s \
-u '<USERNAME>:<PASSWORD>' \
'http://<HOST>:15672/api/definitions/<VHOST_NAME>' \
-o /backup/rabbitmq/rabbitmq_vhost_<VHOST_NAME>_$(date +%Y%m%d_%H%M%S).json
Example — backing up the vhost app_production:
curl -s \
-u 'admin:your_password' \
'http://10.0.1.10:15672/api/definitions/app_production' \
-o /backup/rabbitmq/rabbitmq_vhost_production_$(date +%Y%m%d_%H%M%S).json
A per-vhost export leaves out users and global parameters. Use the whole-cluster export when the file is meant for a full migration.
Check the file before you trust it
An export that returned an error page is still a file on disk, so count what it holds:
python3 << 'EOF'
import json
with open("/backup/rabbitmq/rabbitmq_definitions_<TIMESTAMP>.json") as f:
d = json.load(f)
print(f"Vhosts: {len(d.get('vhosts', []))}")
print(f"Users: {len(d.get('users', []))}")
print(f"Exchanges: {len(d.get('exchanges', []))}")
print(f"Queues: {len(d.get('queues', []))}")
print(f"Bindings: {len(d.get('bindings', []))}")
print(f"Policies: {len(d.get('policies', []))}")
EOF
The counts should match the source cluster:
Vhosts: 4
Users: 5
Exchanges: 5
Queues: 8
Bindings: 8
Policies: 3
Restore into a new cluster
Import the file
curl -s -X POST \
-u '<USERNAME>:<PASSWORD>' \
-H "Content-Type: application/json" \
-d @/backup/rabbitmq/rabbitmq_definitions_<TIMESTAMP>.json \
http://<TARGET_HOST>:15672/api/definitions
Example:
curl -s -X POST \
-u 'admin:your_password' \
-H "Content-Type: application/json" \
-d @/backup/rabbitmq/rabbitmq_definitions_20260519_020001.json \
http://10.0.2.10:15672/api/definitions
Read the response code
A successful import returns nothing, so ask for the status code explicitly:
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
-u 'admin:your_password' \
-H "Content-Type: application/json" \
-d @/backup/rabbitmq/rabbitmq_definitions_20260519_020001.json \
http://10.0.2.10:15672/api/definitions
| Code | Meaning |
|---|---|
| 200 | Import succeeded |
| 400 | The JSON file is not valid |
| 401 | Wrong username or password |
| 403 | The user lacks the administrator tag |
Verify the two clusters match
Compare the counts on both sides rather than assuming the import was complete:
python3 << 'EOF'
import json, urllib.request, base64
def get_definitions(host, user, password):
url = f"http://{host}:15672/api/definitions"
req = urllib.request.Request(url)
token = base64.b64encode(f"{user}:{password}".encode()).decode()
req.add_header("Authorization", f"Basic {token}")
with urllib.request.urlopen(req) as r:
return json.load(r)
SOURCE = get_definitions("10.0.1.10", "admin", "your_password")
DEST = get_definitions("10.0.2.10", "admin", "your_password")
keys = ["vhosts", "users", "exchanges", "queues", "bindings", "policies"]
print(f"{'Thành phần':<15} {'Nguồn':>8} {'Đích':>8} {'Khớp':>8}")
print("-" * 45)
for k in keys:
src = len(SOURCE.get(k, []))
dst = len(DEST.get(k, []))
match = "OK" if src == dst else "LỆCH"
print(f"{k:<15} {src:>8} {dst:>8} {match:>8}")
EOF
Expected result:
Thành phần Nguồn Đích Khớp
---------------------------------------------
vhosts 4 4 OK
users 5 5 OK
exchanges 5 5 OK
queues 8 8 OK
bindings 8 8 OK
policies 3 3 OK
What to expect from an import
Passwords survive. The definitions file stores password hashes rather than plaintext, and those hashes import as they are. Users keep their existing passwords on the target cluster.
HA policies need the nodes to back them. A ha-mode: all policy imports onto any cluster, but it only takes effect where there are at least three nodes. Give the target cluster a topology comparable to the source.
Importing twice is safe. The operation is idempotent: resources that already exist are updated or left alone, never deleted. Re-running a failed import does no harm.
The cluster name is not overwritten. cluster_name in the file is informational. The target cluster keeps its own name.
Automate it
The backup script
Create /opt/backup/rabbitmq_backup.sh:
#!/bin/bash
# ---- Configuration ----
HOST="<RABBITMQ_HOST>"
USER="admin"
PASS='your_password'
BACKUP_DIR="/backup/rabbitmq"
RETENTION_DAYS=30
# ------------------
mkdir -p "$BACKUP_DIR"
FILENAME="rabbitmq_definitions_$(date +%Y%m%d_%H%M%S).json"
FILEPATH="${BACKUP_DIR}/${FILENAME}"
curl -s \
-u "${USER}:${PASS}" \
"http://${HOST}:15672/api/definitions" \
-o "$FILEPATH"
if [ $? -eq 0 ] && [ -s "$FILEPATH" ]; then
echo "[OK] Backup thành công: ${FILENAME}"
find "$BACKUP_DIR" -name "rabbitmq_definitions_*.json" \
-mtime +${RETENTION_DAYS} -delete
else
echo "[FAIL] Backup thất bại!"
rm -f "$FILEPATH"
exit 1
fi
Make it executable:
chmod +x /opt/backup/rabbitmq_backup.sh
Quote PASS with single quotes. A password containing ! triggers bash history expansion inside double quotes and the script will send the wrong credentials.
Schedule and watch it
crontab -e
Add a daily run at 02:00:
0 2 * * * /opt/backup/rabbitmq_backup.sh >> /var/log/rabbitmq_backup.log 2>&1
Then check the log after the first scheduled run:
tail -f /var/log/rabbitmq_backup.log
Output of each successful run:
[OK] Backup thành công: rabbitmq_definitions_20260519_020001.json
Next steps
- Backup & Restore overview to see which engines still have platform backup
- Create a database when you need the target cluster this page imports into
- Manual backup and restore for Kafka if you also run Kafka, where messages can be captured too