Fleet tracking is one of the most demanding real-time workloads you can build. You have a large number of devices, each publishing position updates at a high rate, and a much smaller number of dashboards that need to display a subset of those devices in real time. The architecture has to handle constant inbound telemetry, selective outbound delivery, variable device connectivity, and occasional high-priority alert messages, all at the same time.
This post walks through a practical architecture for fleet tracking at the 100,000-device scale, including topic design, QoS choices, reconnection handling, and how to keep your infrastructure from becoming a bottleneck.
The Core Challenge
Fleet tracking has a few properties that make it harder than most real-time workloads.
Volume. A fleet of 100,000 vehicles publishing GPS positions every 5 seconds generates 20,000 messages per second. That is constant, not bursty. Your infrastructure needs to sustain that throughput indefinitely, not just survive a spike.
Fanout asymmetry. A single dispatcher dashboard might be watching 50 vehicles. Another might be watching 200. A fleet manager wants an overview of all 100,000. The delivery requirements vary enormously between subscribers, but the ingest pipeline is the same for all devices.
Variable connectivity. Fleet devices are not sitting on a stable office Wi-Fi connection. They are in tunnels, switching between cellular towers, parked in areas with poor signal, and sometimes simply powered off for hours at a time. Your architecture needs to handle reconnections gracefully without losing critical events.
Mixed message criticality. A GPS position update that is 10 seconds old is almost worthless. A geofence breach alert from 10 seconds ago is still very much worth delivering. These two message types need different handling.
Topic Design: Fleet vs Vehicle
The first architecture decision is how to organize the fleet. There are two main patterns.
One room per vehicle: Each device publishes into its own room, for example vehicle-v_abc123. Dashboards subscribe to the individual rooms for the vehicles they are tracking.
One room per fleet with filtered subscriptions: All vehicles in a fleet publish to a shared positions topic in one room, for example fleet-f_xyz, tagging each publish with the vehicle id as a filter. Dashboards subscribe to the topic with a filter list specifying which vehicle ids they want updates for.
For most workloads, the filtered subscription approach is better. It keeps the subscription count manageable. A dashboard watching 200 vehicles opens one subscription with 200 filters, rather than 200 individual subscriptions. Subscription overhead adds up: connection tracking, heartbeat management, and routing table size all scale with subscription count. Topic names are single tokens (no /), so the hierarchy is exactly app/room/topic and the filter carries the per-vehicle routing key. NoLag caps a subscription at 100 filters, so a dashboard watching more than that opens a second subscription or narrows its view.
import { NoLag } from '@nolag/js-sdk'
// KRAKEN_URL is your broker, for example ws://localhost:8080/ws
const client = NoLag(DEVICE_TOKEN, { url: KRAKEN_URL })
await client.connect()
// APP_SLUG and the fleet room are granted to this token in your auth config
const room = client.setApp(APP_SLUG).setRoom('fleet-f_xyz')
// Device publishes its position, tagged with its vehicle id as the filter
room.emit(
'positions',
{
vehicleId: 'v_abc123',
lat: -33.8688,
lng: 151.2093,
speed: 62,
heading: 287,
timestamp: Date.now(),
},
{ qos: 0, filter: 'v_abc123' }, // fire and forget for position updates
)One rule to keep in mind: a subscriber with no filters is a wildcard and receives everything on the topic, and a publish with no filter reaches only those wildcard subscribers. Filters are routing, not a privacy boundary. The exception to the shared-room design is very large fleets where different parts of the fleet are managed by different teams. In that case, per-sub-fleet rooms make access control simpler, with each team's actors only granted access to their own fleet's room.
QoS Choices for Fleet Data
Not all fleet messages have the same delivery requirements. Getting QoS right is important both for reliability and for not overloading your infrastructure with unnecessary acknowledgment traffic. On NoLag the QoS level you pass with a publish (0, 1 or 2, default 1) applies to the hop between kraken and an MQTT broker, when you run kraken with its MQTT backend; the built-in broker does not use it. The WebSocket leg between the device and kraken has an optional published acknowledgement, surfaced as the emit callback, and no resend.
QoS 0 (fire and forget) for position telemetry. GPS positions are high-frequency and time-sensitive. A position from 30 seconds ago has almost no value. If a packet drops, the next position update will arrive within seconds anyway. Using QoS 1 for position updates would mean acknowledging and retrying thousands of messages per second inside the broker, most of which would be stale by the time they are retried. QoS 0 is the right choice.
QoS 1 (at least once) for geofence alerts. When a vehicle breaches a geofence, that event needs to be delivered. Missing a geofence breach can mean a compliance violation or a missed theft alert. On an MQTT backend, QoS 1 has the broker hop acknowledge the message and retry it if that delivery fails, and on any backend the emit callback tells the device the broker accepted it.
// Geofence breach: use QoS 1 and wait for the broker's acknowledgement
room.emit(
'alerts',
{
type: 'geofence_breach',
vehicleId: 'v_abc123',
geofenceId: 'zone_restricted_01',
breachType: 'exit',
timestamp: Date.now(),
},
{ qos: 1, filter: 'v_abc123' },
(err) => {
if (err) console.error('alert not accepted by the broker', err)
},
)Separating alerts onto their own topic also lets dashboards manage two subscriptions with different characteristics: a high-volume QoS 0 feed for position updates, and a low-volume QoS 1 feed for alerts that demands attention.
Reconnection for Spotty Devices
Fleet devices lose connectivity constantly. A delivery truck going through a basement loading dock, a train in a tunnel, a vehicle switching between cellular providers in a rural area. The connection drops and eventually comes back.
For position data, reconnection is simple: the device reconnects and starts publishing again. Nothing needs to be caught up. The dashboard gap-fills naturally as new positions arrive.
For alert data, it is different. If a geofence breach happened while the device was offline, the device needs to publish that event when it reconnects. The device should buffer critical events locally and flush them on reconnect, publishing them with their original timestamps so the backend can process them in the correct order.
import type { RoomContext } from '@nolag/js-sdk'
type AlertEvent = {
type: string
vehicleId: string
geofenceId?: string
timestamp: number
}
// Device-side: buffer critical events during outage
class EventBuffer {
private queue: AlertEvent[] = []
enqueue(event: AlertEvent) {
this.queue.push(event)
this.persistToStorage() // survive process restarts
}
flush(room: RoomContext) {
while (this.queue.length > 0) {
const event = this.queue[0]
room.emit('alerts', event, { qos: 1, filter: event.vehicleId })
this.queue.shift()
}
}
private persistToStorage() {
// write this.queue to disk
}
}
const buffer = new EventBuffer()
// 'connect' fires after every successful reconnect ('reconnect' fires when a
// retry is scheduled, before the socket is back)
client.on('connect', () => buffer.flush(room))On the dashboard side there is no replay to lean on. NoLag does not replay missed messages: a dashboard that reconnects receives what is published from then on and nothing it missed, and there is no protocol call to request messages from a sequence number. The durable copy of every alert therefore belongs in your backend: a service subscribed to alerts writes each one to a database, and the dashboard reads the open alerts from there on load. A hydration webhook makes the load step automatic: when a dashboard subscribes to alerts, the broker calls your webhook and delivers its response to the client as a hydration event, so the current alert list arrives on the same connection as the live feed.
Scaling Considerations
At 100,000 devices publishing every 5 seconds, the numbers look like this: 20,000 inbound messages per second, each message fanout to potentially hundreds of dashboard subscriptions. If each dashboard is watching 50 vehicles out of 100,000, the filter match rate is 0.05%. Each filter is its own routing key on the broker, so a position tagged v_abc123 is only delivered to subscriptions that asked for v_abc123 (and to wildcard subscribers); nothing is evaluated per message per dashboard. The infrastructure needs to do that routing fast and cheaply.
At this volume, run more than one kraken node. kraken's built-in broker is a starter broker for a single region and modest scale; point its broker slot at an MQTT broker you operate (EMQX, for example) and run several kraken nodes in front of it, as the scaling guide describes. Then measure: the numbers in this post are an architecture sketch, not a NoLag benchmark.
A few design choices help at this scale:
- Keep payloads small. A GPS position needs only lat, lng, speed, heading, and a timestamp. That is under 100 bytes. Do not attach full vehicle records or lookup data to telemetry messages.
- Let the SDK's binary encoding work for you. NoLag SDKs encode every frame as MessagePack, so a numeric-heavy position is already smaller on the wire than the same object as JSON text. Keep field names short and values numeric to get the most from it.
- Separate telemetry from alerts. Position updates and geofence alerts should be on different topics with different QoS. Mixing them makes it impossible to give alerts an acknowledged hop without paying for it on every position.
- Limit dashboard subscription scope. A dashboard watching all 100,000 vehicles is the worst case for fanout. Design the UX to encourage focused views, for example by region or by job, so that each subscription filter list stays manageable and under the 100-filter cap.
How NoLag Approaches IoT Workloads
NoLag is built for asymmetric fanout workloads like fleet tracking. Filters are routing keys on the broker, so subscribers only receive what they asked for and a message that no dashboard is watching is never transmitted. QoS levels are set per message, so you can mix fire-and-forget telemetry and acknowledged alerts on the same connection without any special configuration.
For dashboard reconnects, subscribe in the connect handler, which runs after every successful reconnect, and refresh anything stateful there. For a fleet manager who closes their laptop and comes back in the morning, pair that with a hydration webhook so the open-alerts list arrives with the subscription, and keep the durable record of alerts in your own store.
For device-side connectivity, the SDK handles exponential backoff reconnection automatically. Devices that lose signal and regain it will reconnect and resume publishing without any application-level retry code, and the event buffer above covers the alerts raised while they were dark.