The cleanest way to run MQTT across dev, staging and production is simple to state: give each environment its own broker (or isolated broker instance) and its own credentials, keep topic names identical across environments, and change only configuration — URL, username, password — when code moves from one to the next. Everything else in this guide is about making that boring and safe.
What each environment is for
| Dev | Staging | Production | |
|---|---|---|---|
| Purpose | Fast iteration, experiments | Release candidate, integration and load tests | Real devices and customers |
| Broker | Local Docker or a shared test endpoint | Same broker type and settings as prod | Dedicated, monitored |
| Data | Fake, disposable | Realistic, generated or anonymized | Real |
| Who connects | Developers, simulators | CI, test devices, QA | Fleet and backend services |
| Credentials | Per developer or shared dev user | Per service, staging only | Per service and per device |
| TLS | Optional | Required, same as prod | Required |
Staging is only useful if it behaves like production. A staging broker without TLS, without authentication or with wide-open ACLs will happily pass releases that fail on day one in production.
Three ways to separate environments
| Model | How it works | Good for | Watch out |
|---|---|---|---|
| One broker per environment | Separate hosts or instances; topics identical | Production, and any team that can afford it | More endpoints to manage |
| Shared broker, prefixed topics | dev/..., stg/... on one broker, ACLs per env | Dev and staging on a budget | One misconfigured ACL leaks data across envs; shared capacity |
| Hybrid | Prod isolated; dev and stg share one broker | Most small and mid-sized teams | Keep prod credentials out of the shared broker entirely |
Whatever you pick, production never shares a broker with tests. A load test, a runaway simulator or a retained message published by mistake should not be able to reach real devices.
Topic prefixes done right
With one broker per environment, do not put the environment in the topic at all. factory/line1/temp is the same topic in staging and production, so subscriptions, dashboards, rules and ACL files can be promoted without edits.
When environments share a broker, a prefix is required — and it must be added in one place. The worst pattern is environment checks scattered through the code (if env == "prod": topic = ...). Build every topic through a helper that prepends the configured prefix, and keep the prefix empty when the environment has its own broker:
import mqtt from "mqtt";
export const ENV = process.env.APP_ENV ?? "dev";
const url = process.env.MQTT_URL;
const prefix = process.env.MQTT_TOPIC_PREFIX ?? ""; // "" when the env has its own broker
if (!url) throw new Error("MQTT_URL is not set");
export const topic = (t) => (prefix ? `${prefix}/${t}` : t);
export function connect(name) {
return mqtt.connect(url, {
clientId: `${name}-${ENV}-${Math.random().toString(16).slice(2, 8)}`,
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
protocolVersion: 5,
reconnectPeriod: 2000,
});
}import { connect, topic } from "./mqtt-config.mjs";
const client = connect("ingest");
client.on("connect", () => client.subscribe(topic("factory/+/temp"), { qos: 1 }));Including the environment in the client ID also makes broker logs readable: you see at once that ingest-stg-3fa9c1 is a staging service. For a shared dev broker, give each developer a personal prefix such as testmqtt/dev-alice so experiments never collide — the details are in naming test topics so tests never collide.
Credentials and ACLs per environment
Every environment gets its own users, and a credential from one environment must be useless in another. Name users after service and environment (ingest-stg, ingest-prod) so a leaked or misused credential is obvious in the logs.
On a shared dev/staging broker, ACLs are what actually keeps environments apart. A Mosquitto ACL file for that case:
user ingest-dev
topic read dev/telemetry/#
topic write dev/cmd/#
user ingest-stg
topic read stg/telemetry/#
topic write stg/cmd/#Verify the separation rather than trusting it: connect with the dev user and try to subscribe to stg/#. With MQTT 5 the broker returns a SUBACK failure code (0x87 Not authorized); with MQTT 3.1.1 you get 0x80 or simply no messages. Testing MQTT ACL permissions shows how to automate these negative tests.
Configuration per environment
Keep one config file per environment and load it at startup; the code stays the same.
APP_ENV=dev
MQTT_URL=mqtt://broker.connectmq.com:1883
MQTT_USER=username
MQTT_PASS=password
MQTT_TOPIC_PREFIX=testmqtt/dev-aliceAPP_ENV=staging
MQTT_URL=mqtts://<your-staging-host>:8883
MQTT_USER=ingest-stg
MQTT_PASS=<from your secret store>
MQTT_TOPIC_PREFIX=Production values live only in the secret store of the production deployment, never in the repository and never in CI test jobs. Add a guard to the test setup so a mistake cannot point the suite at production:
import { ENV } from "../mqtt-config.mjs";
if (ENV === "prod" || ENV === "production") {
throw new Error("Refusing to run tests against production");
}Devices need the same discipline. Firmware should not hard-code a broker; the environment is assigned at provisioning time together with the device's credentials, so a staging device cannot end up in the production fleet. See MQTT device provisioning.
Promoting broker configuration
Broker configuration — listeners, TLS settings, limits, ACL files — should move through environments the same way code does:
- Keep the broker config in git as one template plus per-environment values (hostnames, certificate paths, limits). The Mosquitto config generator is a quick way to produce a correct starting file.
- Review ACL changes like code; a single
#in the wrong line opens everything. - Apply the change to dev, then staging, run the tests, and only then promote the same commit to production.
- Diff staging against production before every release. Any difference that is not a hostname, a secret or a capacity limit is a bug waiting to happen.
Smoke-test every environment
After each deployment, run one end-to-end round trip with that environment's own credentials. The same script works everywhere:
#!/usr/bin/env bash
set -euo pipefail
: "${MQTT_HOST:?}" "${MQTT_USER:?}" "${MQTT_PASS:?}"
T="${MQTT_TOPIC_PREFIX:-testmqtt}/smoke/$(date +%s)"
OUT=$(mktemp)
mosquitto_sub -h "$MQTT_HOST" -p 8883 --capath /etc/ssl/certs \
-u "$MQTT_USER" -P "$MQTT_PASS" -t "$T" -C 1 -W 10 > "$OUT" &
sleep 1
mosquitto_pub -h "$MQTT_HOST" -p 8883 --capath /etc/ssl/certs \
-u "$MQTT_USER" -P "$MQTT_PASS" -t "$T" -m ok -q 1
wait
grep -qx ok "$OUT" && echo "smoke test passed on $MQTT_HOST"-W 10 makes the subscriber exit with an error if nothing arrives within ten seconds, so a broken environment fails the pipeline instead of hanging it. For a richer version, see an MQTT smoke test script; for a manual check, connect with the online MQTT client using the environment's credentials.
Common mistakes
- One set of credentials everywhere. A leaked dev password becomes a production incident.
- Staging without TLS or auth. Certificate and permission bugs then appear for the first time in production.
- Environment checks in business logic. Promote configuration, not code branches.
- Retained test data in a shared broker. Clear it after test runs, or later runs read stale state.
- Forgotten staging devices. Test hardware pointed at production by an old config pollutes real data.
Before go-live, run through the MQTT production checklist against the production environment itself, not staging.
Separate brokers, separate credentials, identical topics and one place where configuration changes: with those four rules, moving a release from dev to staging to production is a config switch and a smoke test, not a migration.
Frequently asked questions
Should dev, staging and production share one MQTT broker?
Production should have its own broker or isolated instance. Dev and staging can share one broker if every environment has its own credentials, its own topic prefix and ACLs that keep them apart, but separate instances are simpler to reason about.
Should the environment name be part of the MQTT topic?
Only when environments share a broker. With one broker per environment, keep topics identical everywhere so code and dashboards are promoted unchanged. If you must prefix, add the prefix in exactly one place in the client code.
How do I stop tests from publishing to production?
Give production credentials only to production services, keep them out of CI test jobs, and add a guard in the test setup that refuses to run when the configured environment or broker URL is production.


