Architecture / SaaS / FeathersJS / Node.js / MQTT / Real-Time
Scaling Real-Time Tracking Systems: Lessons from Container Logistics
How we built a real-time truck and container tracking system handling thousands of IoT events via MQTT — architecture decisions, service separation, and high-performance geofencing.
|9 min read
Introduction
For the past several years, I've worked on a real-time tracking system for CDAS in Singapore — tracking trucks and containers as they move between depots, ports, and delivery points. The system processes thousands of GPS events per minute and pushes live updates to dispatchers watching a map interface.
This post covers the architecture we built, focusing on how we separated concerns between mobile app tracking and high-frequency IoT data.
System Overview
The tracking system is split into specialized components to handle different data sources, reliability requirements, and sync intervals:
- Tracking Service — The core engine that collects data from both Mobile (REST) and IoT hardware (MQTT).
- Redis State Store — Stores the "Last Known Position" from both sources as distinct records for comparison and fallback.
GPS Ingestion via MQTT
Instead of a custom TCP ingestion layer, we leveraged MQTT (via a specialized broker) for our hardware trackers. This allowed us to handle thousands of concurrent connections with minimal overhead. The tracking engine service subscribes to tracker and gate topics, processing events as they arrive.
1
// Data normalization from MQTT in the Tracking Engine
2
const isMqttData = !!data['device.id'];
3
if (isMqttData) {
4
const recordedAt = new Date(parseInt(data['server.timestamp']) * 1000).toISOString();
5
trackData = {
6
vehicleNo: data['device.name'],
7
location: { type: 'Point', coordinates: [data['position.longitude'], data['position.latitude']] },
8
recordedAt,
9
heading: data['position.direction'],
10
speed: data['position.speed'],
11
source: 'gps'
12
};
13
}
State Store & Persistence
We hit a bottleneck early on: writing high-frequency GPS pings (every few seconds per truck) directly to MongoDB caused significant write pressure and database bloat.
The Hybrid Storage Strategy:
- Real-time State (Redis): Every ping updates the vehicle's current location in Redis using Geospatial indexes (
GEOADD). This allows for sub-millisecond proximity queries. - Historical Logs (CSV): For high-frequency hardware data, we bypass the database entirely. The engine appends raw data to local CSV files. These files are later rotated, archived, and moved to S3 for long-term storage and reporting.
- App-based Tracking (MongoDB): Tracking data from the mobile app (which has a lower frequency) is still stored in MongoDB by the logistics service, providing a reliable audit trail for specific driver actions.
Event Processing & Geofencing
One of the most critical features is detecting when a truck enters or exits a depot. We use the Turf.js library for high-performance spatial analysis.
Geofencing Pipeline
When a location update arrives, the engine performs a point-in-polygon check against known depot boundaries. If a transition is detected (Entry or Exit), it triggers an automated notification to the port authorities.
1
// Entry/Exit detection using @turf/turf
2
const entryExitTestResult = await this.detectEntryOrExit(vehicleNo, coordinates);
3
4
if (entryExitTestResult.entry) {
5
await this.triggerEntryExitNotification(trackData, 'N', entryExitTestResult.entry.depotC);
6
} else if (entryExitTestResult.exit) {
7
await this.triggerEntryExitNotification(trackData, 'X', entryExitTestResult.exit.depotC);
8
}
Scaling with FeathersJS
We used FeathersJS to build both services. Its built-in support for Socket.io and feathers-sync (via Redis) made scaling horizontal clusters straightforward. When the tracking engine updates the current location in Redis, the event is synchronized across all service instances, ensuring dispatchers receive real-time updates regardless of which server they are connected to.
What Broke First
The first real outage was due to Redis write pressure. We were updating too many keys individually per GPS ping. We optimized this by batching certain updates and utilizing Redis Hashes (HSET) to group vehicle attributes, significantly reducing the command overhead on our Redis cluster.
Another lesson: Hardware diversity is hard. Different trackers send data in different formats (some as floats, some as strings). We had to implement a robust normalization layer with strict type casting to ensure our geofencing logic didn't fail on "undefined" coordinates.
Conclusion
By separating the high-frequency tracking engine from the main business logic, we built a system that is both scalable and resilient. Using MQTT for ingestion and CSV/S3 for historical logs allowed us to handle thousands of events per minute without overwhelming our primary database.
For more on the systems I've designed and shipped, see my engineering background, or read Multi-Tenant SaaS Architecture Lessons for the platform that ran on top of this tracking engine.
Written by Erik Yuntantyo·Software Engineer·About me