Tutorials

MQTT in Flutter with mqtt_client: Mobile, Desktop and Web

Connect a Flutter app to an MQTT broker with mqtt_client: server vs browser client, TLS on 8883, secure WebSockets for Flutter web, reconnects and testing.

Updated · 9 min read

MQTT in Flutter with mqtt_client: Mobile, Desktop and Web

To use MQTT in Flutter, add the mqtt_client package, create an MqttServerClient for Android, iOS and desktop or an MqttBrowserClient for Flutter web, connect, subscribe and listen to the updates stream. The one thing that trips most people up is that the two clients use different transports: the server client speaks plain TCP or TLS, while the browser client can only use WebSockets.

This tutorial builds a small, reusable MQTT service for Flutter, shows the TCP, TLS and secure WebSocket configurations against broker.connectmq.com, and ends with a quick way to test the whole round trip from your browser.

Install mqtt_client

mqtt_client is a pure Dart package, so it works on every Flutter platform without native plugins:

Add the dependency
flutter pub add mqtt_client

The package contains two client classes in two separate libraries. Which one you import decides which transport you get:

ClassImportPlatformsTransports
MqttServerClientpackage:mqtt_client/mqtt_server_client.dartAndroid, iOS, macOS, Windows, LinuxTCP, TLS, WebSocket, secure WebSocket
MqttBrowserClientpackage:mqtt_client/mqtt_browser_client.dartFlutter webWebSocket, secure WebSocket only

mqtt_server_client.dart imports dart:io, which does not exist in the browser, so importing it in a web build fails at compile time. We will solve that with a conditional import further down. If you are writing a pure Dart command-line tool rather than an app, the same API applies — see MQTT in Dart.

First connection over TCP

Start with the simplest setup: plain TCP on port 1883. Every client needs a unique client ID; if two devices share one, the broker disconnects the older session each time the other connects. The client ID generator is handy for picking a format.

lib/mqtt_demo.dart
import 'dart:math';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';

Future<MqttServerClient> connectMqtt() async {
  final clientId = 'flutter-${Random().nextInt(0xFFFFFF).toRadixString(16)}';
  final client = MqttServerClient.withPort('broker.connectmq.com', clientId, 1883);

  client.logging(on: false);
  client.setProtocolV311();
  client.keepAlivePeriod = 30;
  client.connectTimeoutPeriod = 5000; // milliseconds
  client.onConnected = () => print('connected');
  client.onDisconnected = () => print('disconnected');

  client.connectionMessage = MqttConnectMessage()
      .withClientIdentifier(clientId)
      .startClean()
      .withWillTopic('testmqtt/flutter-demo/$clientId/status')
      .withWillMessage('offline')
      .withWillQos(MqttQos.atLeastOnce);

  try {
    await client.connect(); // or client.connect('username', 'password')
  } on Exception catch (e) {
    print('connect failed: $e');
    client.disconnect();
    rethrow;
  }

  final state = client.connectionStatus?.state;
  if (state != MqttConnectionState.connected) {
    throw Exception('broker refused connection: ${client.connectionStatus}');
  }
  return client;
}

connect() throws NoConnectionException when the broker rejects the CONNECT (wrong credentials, banned client ID) and a SocketException when the host or port cannot be reached. Printing client.connectionStatus gives you the return code, which you can look up in the MQTT reason codes table.

Subscribe and receive messages

Incoming messages arrive on client.updates, a Dart stream. Subscribe first, then listen once — not once per subscription:

Subscribe and listen
const topic = 'testmqtt/flutter-demo/sensors/#';

void listen(MqttServerClient client) {
  client.subscribe(topic, MqttQos.atLeastOnce);

  client.updates!.listen((messages) {
    for (final m in messages) {
      final publish = m.payload as MqttPublishMessage;
      final text = MqttPublishPayload.bytesToStringAsString(publish.payload.message);
      print('${m.topic}: $text');
    }
  });
}

bytesToStringAsString decodes the bytes as UTF-8-ish text. For JSON, pass the result to jsonDecode; for binary payloads (CBOR, Protobuf) use publish.payload.message directly, which is a typed byte buffer.

In a widget, keep the StreamSubscription returned by listen and cancel it in dispose(), or expose the stream through your state-management solution (Provider, Riverpod, Bloc) so that widgets rebuild from a single source of truth instead of each opening its own connection.

Publish

Payloads are built with MqttClientPayloadBuilder:

Publish a JSON reading
import 'dart:convert';

void publishReading(MqttServerClient client, double celsius) {
  final builder = MqttClientPayloadBuilder()
    ..addUTF8String(jsonEncode({'celsius': celsius, 'ts': DateTime.now().toIso8601String()}));

  client.publishMessage(
    'testmqtt/flutter-demo/sensors/kitchen',
    MqttQos.atLeastOnce,
    builder.payload!,
    retain: false,
  );
}

Use addUTF8String rather than addString when text can contain non-ASCII characters; addString writes one byte per UTF-16 code unit and can mangle accented characters and emoji.

TLS on port 8883

For production, switch the server client to TLS. Brokers with a certificate from a public CA, like broker.connectmq.com, are verified against the operating system's trust store, so no extra files are needed:

TLS configuration
import 'dart:io';

final client = MqttServerClient.withPort('broker.connectmq.com', clientId, 8883)
  ..secure = true
  ..securityContext = SecurityContext.defaultContext;

await client.connect('username', 'password');

For a broker with a self-signed or private CA, create a SecurityContext(withTrustedRoots: false) and call setTrustedCertificatesBytes() with the CA certificate loaded from your assets. Avoid onBadCertificate: (_) => true outside a quick local experiment — it accepts every certificate and silently defeats TLS. If the handshake fails, testing the TLS connection from a terminal tells you whether the problem is the broker or the app.

Flutter web: MqttBrowserClient over WSS

In the browser, MQTT runs over WebSockets. Pages served over HTTPS may only open wss:// connections, so use the secure WebSocket port and include the path:

lib/mqtt_browser.dart
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_browser_client.dart';

MqttClient createClient(String clientId) {
  final client = MqttBrowserClient('wss://broker.connectmq.com/mqtt', clientId)
    ..port = 8084
    ..keepAlivePeriod = 30
    ..websocketProtocols = MqttClientConstants.protocolsSingleDefault;
  return client;
}

Note that the port is set as a property, not inside the URL. The websocketProtocols line sends the mqtt subprotocol header, which most brokers expect. Getting the path wrong usually shows up as a WebSocket close right after the upgrade; MQTT over WebSockets explains the handshake in detail.

One code base for all platforms

Create a matching lib/mqtt_server.dart that returns an MqttServerClient (for example on 8883 with TLS), then pick the right file at compile time:

lib/mqtt_factory.dart
export 'mqtt_server.dart'
    if (dart.library.js_interop) 'mqtt_browser.dart';

The rest of the app imports only mqtt_factory.dart and package:mqtt_client/mqtt_client.dart, and works with the common MqttClient base class. Connect, subscribe, publish and the updates stream behave the same on both.

Reconnects and app lifecycle

Mobile networks drop connections constantly: Wi-Fi to cellular handovers, tunnels, the OS suspending your app. mqtt_client has a built-in auto-reconnect:

Auto-reconnect
client
  ..autoReconnect = true
  ..resubscribeOnAutoReconnect = true
  ..onAutoReconnect = () => print('connection lost, reconnecting...')
  ..onAutoReconnected = () => print('reconnected');

With resubscribeOnAutoReconnect (the default is true) the client replays your subscriptions after the reconnect, which matters because a clean session forgets them on the broker side. Messages published while disconnected are not queued by the broker for a clean session, so if the app must not miss data, connect with a persistent session instead (omit startClean(), keep a stable client ID) and subscribe with QoS 1. Test that against your own private broker instance: the shared test endpoint does not keep offline sessions.

Remember that iOS and Android suspend background apps, which closes the socket regardless of keep-alive. Observe the lifecycle with WidgetsBindingObserver and disconnect cleanly in paused, then reconnect in resumed. For background delivery, push notifications are the right tool; see Android background disconnects and iOS background limits.

Platform setup checklist

  • Android: add <uses-permission android:name="android.permission.INTERNET"/> to android/app/src/main/AndroidManifest.xml. Debug builds include it automatically, which is why "works in debug, fails in release" is so common.
  • macOS: add com.apple.security.network.client set to true in both DebugProfile.entitlements and Release.entitlements, or the sandbox blocks outgoing sockets.
  • iOS: nothing extra for TLS. Plain ws:// or TCP to a hostname is not affected by App Transport Security because it applies to URL-loading APIs, but you should still use TLS in production.
  • Web: use MqttBrowserClient, wss://, the right port and path. Mixed content (a ws:// broker from an https:// page) is blocked by the browser.

Test the round trip

Run the app, then open the online MQTT client in a browser, connect to broker.connectmq.com over secure WebSockets and subscribe to testmqtt/flutter-demo/#. You should see the readings your app publishes, and anything you publish from the browser to testmqtt/flutter-demo/sensors/test should appear in the Flutter debug console.

Watch your Flutter app's messages in the browser

If nothing arrives, check in this order: the connection state after connect(), that the subscription topic matches the publish topic (the topic matcher helps with wildcards), and the debug output after client.logging(on: true). The most frequent exceptions and their fixes are collected in Flutter MQTT errors.

With one factory file, one stream of messages and auto-reconnect enabled, the same Flutter code talks to the broker from a phone, a desktop and a browser tab. Start against the shared endpoint, move to TLS and secure WebSockets with credentials, and keep the browser client open while you develop so you can see every message your app sends.

Frequently asked questions

Why does my Flutter MQTT app work on Android but not on the web?

Browsers cannot open raw TCP sockets, so MqttServerClient does not work in Flutter web. Use MqttBrowserClient with a wss:// URL, the broker's WebSocket port (for example 8084) and the correct path such as /mqtt.

Does mqtt_client support MQTT 5?

No. mqtt_client implements MQTT 3.1 and 3.1.1. If you need MQTT 5 features such as reason codes, user properties or session expiry, use the separate mqtt5_client package from the same author, which has a very similar API.

Do I need special permissions to use MQTT in Flutter?

On Android, release builds need the INTERNET permission in AndroidManifest.xml (debug builds add it automatically). On macOS, enable the outgoing network connections entitlement (com.apple.security.network.client). iOS and the web need nothing extra.