Next.js MQTT integration is confusing at first because Next.js runs your code in several places: the browser, a Node.js server, serverless functions and sometimes the edge. In this guide you will learn which of those places can hold an MQTT connection, and you will build the three patterns that cover almost every project: a client component that subscribes over secure WebSocket, a route handler that publishes with server-side credentials, and a Server-Sent Events bridge. Every example uses ConnectMQ's WebSocket endpoint on broker.connectmq.com and MQTT.js v5 with the App Router.
Where Can MQTT Run in Next.js?
MQTT needs a long-lived connection. That single fact decides where it fits:
| Runtime | Long-lived MQTT connection? | Transport | Typical use |
|---|---|---|---|
| Browser (client component) | Yes, while the tab is open | wss:// port 8084, path /mqtt |
Live dashboards, device controls |
Node.js server (next start, container, VM) |
Yes | mqtts:// 8883 or wss:// 8084 |
Shared subscriber, SSE bridge, publishing |
| Serverless function | No, only per request | mqtts:// 8883 |
Publish one command, then disconnect |
| Edge runtime | No | Not supported for MQTT.js TCP | Avoid for MQTT |
| Server Components | No, they render once | - | Read data from a database instead |
A Server Component renders and finishes; it cannot keep a subscription alive. Server Actions are also request-scoped. So the real choice is between connecting from the browser and connecting from a server route that runs on Node.js.
Install the client library once:
npm install mqtt
MQTT.js v5 ships both a Node.js build and a browser build, and bundlers pick the right one automatically. The MQTT.js Node.js guide covers the library itself; here we focus on how it fits into Next.js.
Pattern 1: Subscribe in a Client Component Over WebSocket
For live dashboards, the browser connects straight to the broker. There is no server hop, latency is minimal, and your Next.js server does no work per message. Browsers cannot open raw TCP, so this always uses MQTT over secure WebSocket. Our MQTT over WebSocket article explains the transport.
Create a reusable hook in app/hooks/useMqtt.js:
"use client";
import { useEffect, useRef, useState } from "react";
import mqtt from "mqtt";
const BROKER_URL = process.env.NEXT_PUBLIC_MQTT_URL ?? "wss://broker.connectmq.com:8084/mqtt";
export function useMqtt(topic) {
const clientRef = useRef(null);
const [status, setStatus] = useState("connecting");
const [messages, setMessages] = useState([]);
useEffect(() => {
const clientId = "next-" + Math.random().toString(16).slice(2, 10);
const client = mqtt.connect(BROKER_URL, {
clientId,
protocolVersion: 5,
clean: true,
keepalive: 30,
reconnectPeriod: 3000,
});
clientRef.current = client;
client.on("connect", () => {
setStatus("connected");
client.subscribe(topic, { qos: 1 });
});
client.on("reconnect", () => setStatus("reconnecting"));
client.on("close", () => setStatus("disconnected"));
client.on("message", (t, payload) => {
setMessages((prev) => [...prev.slice(-99), { topic: t, text: payload.toString(), at: Date.now() }]);
});
return () => {
client.end(true);
clientRef.current = null;
};
}, [topic]);
const publish = (t, payload) => clientRef.current?.publish(t, payload, { qos: 1 });
return { status, messages, publish };
}
Use it in a page, app/live/page.jsx:
"use client";
import { useMqtt } from "../hooks/useMqtt";
const PREFIX = "connectmq/demo/next-k7f3q2";
export default function LivePage() {
const { status, messages } = useMqtt(`${PREFIX}/sensors/#`);
return (
<main style={{ padding: 24 }}>
<h1>Live sensors</h1>
<p>Broker: {status}</p>
<ul>
{messages.map((m) => (
<li key={m.at + m.topic}>{m.topic}: {m.text}</li>
))}
</ul>
</main>
);
}
React escapes text in JSX, so rendering payloads like this is safe from script injection. Publish a test reading from a terminal and watch it appear:
mosquitto_pub -h broker.connectmq.com -p 1883 -i next-test-3b1c -t 'connectmq/demo/next-k7f3q2/sensors/room1' -m '{"t":22.4}'
Three details that bite in development
- Strict Mode double mount. In development React mounts, unmounts and remounts components to surface cleanup bugs. Without
client.end()in the effect cleanup, you get two connections. Generating the client ID inside the effect also prevents both instances fighting over the same ID. - Never connect at module scope. A top-level
mqtt.connect()in a client file also runs during server-side rendering of that component, where it opens a TCP connection on your server. Keep connections insideuseEffect, which only runs in the browser. NEXT_PUBLIC_means public. Any variable with that prefix is inlined into the JavaScript bundle. Put only the broker URL there, never a password.
Pattern 2: Publish From a Route Handler With Private Credentials
Commands that change the physical world, such as opening a valve or rebooting a device, should not be sent with credentials that every visitor can read. Instead, the browser calls your API, the server checks the user, and the server publishes with credentials kept in environment variables.
First, a server-side singleton in lib/mqtt-server.js. Caching the client on globalThis prevents hot reload in development from creating a new connection on every file save:
import mqtt from "mqtt";
const globalForMqtt = globalThis;
export function getMqttClient() {
if (!globalForMqtt.__mqttClient) {
globalForMqtt.__mqttClient = mqtt.connect(process.env.MQTT_URL ?? "mqtts://broker.connectmq.com:8883", {
clientId: process.env.MQTT_CLIENT_ID ?? "next-server-" + Math.random().toString(16).slice(2, 10),
username: process.env.MQTT_USERNAME,
password: process.env.MQTT_PASSWORD,
protocolVersion: 5,
clean: true,
reconnectPeriod: 5000,
});
}
return globalForMqtt.__mqttClient;
}
Then the route handler in app/api/command/route.js:
import { getMqttClient } from "@/lib/mqtt-server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const PREFIX = "connectmq/demo/next-k7f3q2";
const ALLOWED = new Set(["on", "off"]);
export async function POST(request) {
const { deviceId, action } = await request.json();
if (!/^[a-z0-9-]{1,32}$/.test(deviceId ?? "") || !ALLOWED.has(action)) {
return Response.json({ error: "invalid command" }, { status: 400 });
}
// Check the user's session or role here before publishing.
await getMqttClient().publishAsync(
`${PREFIX}/devices/${deviceId}/cmd`,
JSON.stringify({ action, at: new Date().toISOString() }),
{ qos: 1 }
);
return Response.json({ ok: true });
}
Your .env.local for a private ConnectMQ broker looks like this:
MQTT_URL=mqtts://YOUR_BROKER_HOST:8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
Validating deviceId against a strict pattern is not optional. If you let user input flow straight into a topic, a request containing + or # or extra slashes could publish to topics you never intended.
On serverless platforms
On serverless hosting, a function instance may be frozen or discarded between requests, so a cached connection cannot be relied on. Connect, publish and close in the same request instead:
import mqtt from "mqtt";
export const runtime = "nodejs";
export async function POST(request) {
const { message } = await request.json();
const client = await mqtt.connectAsync(process.env.MQTT_URL, {
clientId: "next-fn-" + Math.random().toString(16).slice(2, 10),
username: process.env.MQTT_USERNAME,
password: process.env.MQTT_PASSWORD,
});
try {
await client.publishAsync("connectmq/demo/next-k7f3q2/notices", String(message), { qos: 1 });
} finally {
await client.endAsync();
}
return Response.json({ ok: true });
}
This adds a TLS handshake to every request, which is fine for occasional commands but not for high-frequency publishing. Note that ConnectMQ binds client IDs to issued credentials on private brokers, so if you run many short-lived functions concurrently, check how your credentials and client IDs are provisioned in the portal.
Pattern 3: Stream MQTT to the Browser With Server-Sent Events
Sometimes you do not want the browser talking to the broker at all, for example when the topic data needs filtering per user. A Node.js route can hold one subscription and fan messages out to browsers over Server-Sent Events (SSE). This only works on a long-running Node.js server, not on serverless functions with short timeouts.
Subscribe once in the singleton (add this inside getMqttClient after creating the client):
globalForMqtt.__mqttClient.on("connect", () => {
globalForMqtt.__mqttClient.subscribe("connectmq/demo/next-k7f3q2/sensors/#", { qos: 1 });
});
Then add app/api/stream/route.js:
import { getMqttClient } from "@/lib/mqtt-server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET(request) {
const client = getMqttClient();
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
const onMessage = (topic, payload) => {
const data = JSON.stringify({ topic, payload: payload.toString() });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
};
client.on("message", onMessage);
request.signal.addEventListener("abort", () => {
client.off("message", onMessage);
controller.close();
});
},
});
return new Response(stream, {
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
});
}
In a client component, new EventSource("/api/stream") receives each message as an event. One broker connection now serves every viewer, which keeps you comfortably inside connection limits.
Troubleshooting Next.js MQTT Connections
A few problems come up again and again when wiring MQTT into a Next.js project:
- "WebSocket connection failed" in the browser console. Check the URL carefully: it must be
wss://, port 8084, and end in/mqtt. A page served over HTTPS cannot use plainws://. - "Module not found: net" or "tls" during the build. Something pulled server-only code into a client bundle, usually through a shared file that also imports Node.js modules. Keep
lib/mqtt-server.jsout of anything marked"use client". - Route handler hangs, then times out. The broker rejected the login or the port is blocked from your host, so
publishAsyncwaits for a connection that never arrives. Listen to theerrorevent and log it, and test the same credentials withmosquitto_pubfrom the server. - Messages arrive twice on the server. Hot reload created a second client because the singleton was not cached on
globalThis. Restart the dev server after fixing it.
Choosing the Right Pattern
Use this checklist to decide:
- Read-only live data, public or low sensitivity: Pattern 1, browser over WebSocket with read-only credentials.
- Actions with side effects: Pattern 2, route handler with server-side credentials and input validation.
- Per-user filtering or many viewers on a small plan: Pattern 3, SSE bridge on a Node.js server.
- Historical charts: none of the above alone. Store messages with a separate subscriber and query the database from a Server Component.
Most production apps combine 1 and 2: the browser subscribes for live updates, and every command goes through a route handler. For component-level details see the React MQTT tutorial, and for typed payloads read the MQTT with TypeScript guide. The MQTT chat app tutorial is a good example of a purely browser-side design, and the MQTT client libraries overview helps if part of your stack is in another language. The official Next.js route handler documentation covers runtime options in depth.
Connect Your Next.js App to ConnectMQ
ConnectMQ is a fully managed MQTT service with secure WebSocket on 8084 for client components and TLS on 8883 for route handlers, so both sides of a Next.js app talk to the same broker. Prototype against the public MQTT test broker, then move to a private MQTT broker in the cloud. Sign up free on the Serverless plan, no credit card required.
Frequently Asked Questions
Can I use MQTT in Next.js Server Components?
Not for live data. Server Components render once per request and cannot hold a subscription. Subscribe in a client component or a Node.js route handler, and use Server Components to read stored data.
Does MQTT work on the Next.js Edge runtime?
The Edge runtime does not provide the Node.js networking APIs that MQTT.js uses for TCP and TLS. Set export const runtime = "nodejs" on any route that talks to the broker.
How do I keep MQTT credentials out of the browser in Next.js?
Store them in non-public environment variables and only use them in route handlers or server code. Anything prefixed with NEXT_PUBLIC_ ends up in the client bundle.
Why do I see two MQTT connections in development?
React Strict Mode mounts components twice in development, and Next.js hot reload re-executes server modules. Close the client in the effect cleanup and cache server clients on globalThis.



