◆ Blog

MQTT in Flutter with AWS IoT Core: Auth, Reconnects and App Lifecycle

A production checklist for connecting a Flutter app to AWS IoT Core over MQTT — choosing the right auth model, unique client IDs, keep-alive, QoS 1 duplicates, and reconnecting cleanly across app lifecycle changes.

By Vishnu Prasad KP · · 4 min read

Getting a Flutter app to connect to AWS IoT Core takes about twenty lines with the mqtt_client package. Keeping it connected across Wi-Fi drops, app backgrounding and two phones signed into the same account takes a lot more. These are the problems I've had to solve in production IoT apps and how I handle them now.

1. Pick the auth model before writing code

AWS IoT Core accepts several ways to authenticate an MQTT client, and the right one depends on who is connecting:

  • X.509 client certificates (mutual TLS, port 8883). The standard for devices: each device gets its own certificate and private key at provisioning. It's the wrong default for a consumer app. A certificate bundled in an APK or IPA can be extracted, and every install would share one identity.
  • Amazon Cognito + SigV4-signed WebSocket (port 443). Each signed-in user gets temporary AWS credentials scoped by an IoT policy. This fits apps that need direct device access per user.
  • Custom authorizer. IoT Core calls your Lambda to validate a token you already issue, such as your app's JWT. This is useful when you have your own identity system.
  • No direct connection. Your backend subscribes to IoT Core and relays to the app over its own WebSocket. This is often the simplest option when the app only needs a few events.

If you do use certificates, for example in an installer or commissioning app, issue them per install from your backend and keep them in secure storage, not in assets/.

2. A connection you can reason about

With per-install certificates loaded as bytes (from flutter_secure_storage, for example), the connection looks like this:

import 'dart:io';
import 'dart:typed_data';

import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';

Future<MqttServerClient> connectToIot({
  required String endpoint, // xxxxxxxxxxxx-ats.iot.ap-south-1.amazonaws.com
  required String clientId,
  required Uint8List rootCa,
  required Uint8List certificate,
  required Uint8List privateKey,
}) async {
  final context = SecurityContext(withTrustedRoots: false)
    ..setTrustedCertificatesBytes(rootCa)
    ..useCertificateChainBytes(certificate)
    ..usePrivateKeyBytes(privateKey);

  final client = MqttServerClient.withPort(endpoint, clientId, 8883)
    ..secure = true
    ..securityContext = context
    ..keepAlivePeriod = 45
    ..autoReconnect = true
    ..resubscribeOnAutoReconnect = true
    ..logging(on: false);

  client.setProtocolV311();
  client.connectionMessage = MqttConnectMessage().withClientIdentifier(clientId).startClean();

  await client.connect();
  if (client.connectionStatus?.state != MqttConnectionState.connected) {
    client.disconnect();
    throw StateError('AWS IoT connect failed: ${client.connectionStatus}');
  }
  return client;
}

Subscribing and decoding messages:

client.subscribe('chargers/$chargerId/status', MqttQos.atLeastOnce);

client.updates!.listen((messages) {
  for (final received in messages) {
    final publish = received.payload as MqttPublishMessage;
    final json = MqttPublishPayload.bytesToStringAsString(publish.payload.message);
    onStatus(received.topic, json);
  }
});

3. Client IDs must be unique per connection

AWS IoT Core allows only one connection per client ID. When a second client connects with the same ID, the first one is disconnected. If two phones signed into the same account both use user-42 as their client ID, they knock each other offline in a loop, and with autoReconnect enabled that becomes a reconnect storm that looks like flaky networking.

Build the client ID from something that is unique per install, such as app-<userId>-<installId>. Your IoT policy can then require that prefix, so one user can't take over another's ID.

4. Keep-alive: short enough for mobile networks

Mobile carriers and home routers silently drop idle TCP connections. If the keep-alive interval is longer than the NAT timeout, the socket dies without the client noticing, and you stop getting messages with no error at all. AWS IoT Core enforces a keep-alive range (30–1200 seconds at the time of writing). For phones I use 30–60 seconds: frequent enough to keep the connection alive, cheap enough on battery.

5. QoS 1 means "at least once", so expect duplicates

AWS IoT Core supports QoS 0 and QoS 1, not QoS 2. With QoS 1 a message can arrive more than once, especially around reconnects. Make handlers idempotent: include a sequence number or timestamp in the payload and ignore anything older than the state you already have. "Charging stopped" processed twice should be harmless.

6. Tie the connection to the app lifecycle

On mobile, the OS will suspend your socket whenever it likes. Fighting that drains the battery, so work with it: disconnect when the app goes to the background, reconnect when it comes back, then resync from your API, because anything published while you were away is gone (unless you use persistent sessions or retained messages).

class MqttLifecycle with WidgetsBindingObserver {
  MqttLifecycle(this._mqtt);
  final MqttService _mqtt;

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        _mqtt.connect().then((_) => _mqtt.resyncFromApi());
      case AppLifecycleState.paused:
        _mqtt.disconnect();
      default:
        break;
    }
  }
}

Register it with WidgetsBinding.instance.addObserver(...) and remove it on dispose. Also keep a flag for "disconnected on purpose", so that onDisconnected doesn't treat a deliberate disconnect as an error.

7. Design topics around permissions

Put the identity in the topic, such as chargers/{chargerId}/status or users/{userId}/notifications, so IoT policies can grant access with policy variables instead of wildcards. Then a bug in the app can't subscribe to # and read every device in your account. For "what's the latest status?" questions, retained messages give a new subscriber the last value immediately instead of waiting for the next update.

Checklist

  • Choose the auth model per client type; don't ship shared certificates in the app.
  • Use a unique client ID per install.
  • Set keep-alive to 30–60 s on mobile.
  • Expect QoS 1 duplicates and make handlers idempotent.
  • Disconnect on pause, reconnect on resume, then resync from the API.
  • Put identities in topic names and lock them down with policy variables.

I build Flutter apps for IoT and connected hardware. See IoT app development or get in touch.

Building something similar?

I build Flutter apps for IoT, EV-charging and connected hardware — freelance or full-time, from Malappuram, Kerala, for clients anywhere.