IoT Tutorials

Next.js MQTT: Server and Client Integration Patterns That Actually Work

Learn the three ways to use MQTT in a Next.js App Router project: client components over secure WebSocket, server route handlers that publish with private credentials, and a Server-Sent Events bridge.

Diagram of next.js mqtt integration with client components, route handlers and a cloud MQTT broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

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

  1. 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.
  2. 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 inside useEffect, which only runs in the browser.
  3. 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 plain ws://.
  • "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.js out 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 publishAsync waits for a connection that never arrives. Listen to the error event and log it, and test the same credentials with mosquitto_pub from 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:

  1. Read-only live data, public or low sensitivity: Pattern 1, browser over WebSocket with read-only credentials.
  2. Actions with side effects: Pattern 2, route handler with server-side credentials and input validation.
  3. Per-user filtering or many viewers on a small plan: Pattern 3, SSE bridge on a Node.js server.
  4. 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.

Related articles

Best MQTT CLI Tools for Testing, Scripting and Debugging

A hands-on comparison of mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, the MQTT.js CLI, curl and mqttui, with the same test run against broker.connectmq.com so you can choose the right tool for scripting and debugging.

8 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.