Why I Chose Go for a 10,000-Vehicle Real-Time Telematics System
A client hired me to architect their fleet telematics platform. Before picking a database, cloud provider, or system architecture, I had to pick a language.
The requirements: 10,000+ vehicles sending telemetry every 30 seconds, real-time anomaly detection, WebSocket connections for live dashboards, gRPC for inter-service communication, and strict SLA guarantees. The wrong language choice is a performance ceiling you hit in six months and spend the next year fighting.
I chose Go.
Goroutines vs Threads vs Async
At 10,000 vehicles sending 2 updates per minute, the sustained load is around 333 events per second, with spikes well above.
Each event needs to:
- ·Be received over MQTT or WebSocket
- ·Be validated and parsed
- ·Be written to PostgreSQL
- ·Be checked against geofence rules
- ·Be scored for anomaly likelihood
- ·Potentially be written to Redis for the real-time dashboard
- ·Potentially be sent to an alert queue
You need real concurrency for this. The options:
Python asyncio: Works for I/O-bound tasks but the GIL prevents parallelizing CPU-bound work without multiprocessing, which adds significant complexity and memory overhead.
Node.js: Good for I/O-bound concurrency, poor for CPU-intensive computation. Anomaly detection scoring needs real computation.
Java or JVM: Excellent concurrency model. But startup time, memory footprint, and operational complexity for a small team made the calculus harder.
Go: Goroutines are multiplexed over OS threads by the runtime. Spawning 10,000 goroutines (one per vehicle connection) uses roughly 80KB of memory each, so 800MB for 10,000 connections. JVM threads run about 1MB each, so roughly 10GB. The math is clear.
The MQTT Architecture
Each vehicle publishes telemetry to an MQTT topic: vehicles/{vehicle_id}/telemetry.
The ingestion service spawns a goroutine per subscription:
func (s *IngestionService) StartConsumer(ctx context.Context) error {
for {
select {
case msg := <-s.mqttClient.Messages():
go s.processTelemetry(ctx, msg)
case <-ctx.Done():
return ctx.Err()
}
}
}
Each processTelemetry call runs in its own goroutine. Go's scheduler handles the concurrency.
gRPC for Inter-Service Communication
The platform has four services:
- ·Ingestion: receives MQTT messages
- ·Storage: writes to PostgreSQL and Redis
- ·Analytics: real-time anomaly detection
- ·Dashboard: WebSocket server for operator UIs
gRPC in Go is first-class. Service definitions live in protobuf, code generation is automatic, and the performance characteristics hold up at this volume.
service TelemetryService {
rpc IngestReading (TelemetryReading) returns (IngestResponse);
rpc StreamVehicleState (VehicleStateRequest) returns (stream VehicleState);
}
The StreamVehicleState RPC powers the live dashboard. The dashboard service opens a long-lived gRPC stream per vehicle being monitored, and the storage service pushes state updates as they arrive.
What I Would Change
Go's strength for this type of system is real. The one friction point: anomaly detection scoring needed more sophisticated ML than Go expressed cleanly without calling out to a Python service. We ended up with a thin Go-to-Python gRPC call for ML scoring, which added a round-trip but kept both languages in their sweet spots.
Starting over, I would use the same approach. The difference: design the ML service interface from day one rather than retrofitting.