Skip to content
esc
↑ ↓ navigate↵ select
All posts

Streaming Live GPS With Kafka, Redis and Socket.io

3 min read

  • Kafka
  • Socket.io
  • Redis
  • PostGIS
  • Node.js
On this page

On a school bus tracking platform, "real-time" isn't a buzzword. A parent standing at the stop is watching a dot on a map. If it lags, they stop trusting it. The target we held ourselves to was sub-5-second p95 location latency across the active fleet.

Here's how the pieces fit, and why each one is there.

The Shape of the Pipeline

device ──► ingest API ──► Kafka ──► consumer ──┬──► Redis (latest position)
                                               ├──► PostGIS (history, geofences)
                                               └──► Socket.io ──► admin & parent apps

Each stage does one job, so each one can be scaled or restarted without taking the others down.

Why Kafka in the Middle

Devices report constantly, and they don't wait for your database to be in a good mood. Putting Kafka between ingestion and processing gives you:

  • A buffer. Bursts, like every bus starting its route at 7am, queue up instead of timing out.
  • Ordering per bus. Keying messages by vehicle ID keeps each bus's points in order within a partition.
  • Replay. If a consumer has a bug, fix it and re-read the topic.
// Simplified sketch with kafkajs
await producer.send({
  topic: "gps.positions",
  messages: [{ key: vehicleId, value: JSON.stringify({ vehicleId, lat, lng, ts }) }],
});

Redis for "Where Is It Right Now?"

Most reads only need the latest point per bus. Hitting a history table for that is wasteful, so the consumer keeps the current position in Redis. Maps load instantly, and the database only handles the questions that need history.

PostGIS for the Spatial Questions

"Has this bus entered the school zone?" and "which stops are within 500 m?" are spatial queries. PostGIS with a spatial index answers them without scanning every row:

CREATE INDEX idx_positions_geom ON positions USING GIST (geom);

SELECT s.id
FROM stops s
WHERE ST_DWithin(s.geom::geography, ST_MakePoint(:lng, :lat)::geography, 500);

Socket.io Rooms Keep It Private

Not everyone should see every bus. Each client joins only the rooms it's authorized for, like a school's fleet for an admin or one route for a parent. The consumer emits to that room alone:

io.to(`route:${routeId}`).emit("position", { vehicleId, lat, lng, ts });

The same RBAC that guards the REST API decides which rooms a socket may join, at connection time, from the verified token.

Measure the Number You Promise

"Feels fast" isn't a target. We tracked end-to-end latency as device timestamp → client receive time, looked at the p95 rather than the average, and treated regressions as bugs. Averages hide the slow tail, and the slow tail is exactly the parent who's still waiting.

The Takeaway

Split the pipeline by responsibility: Kafka absorbs and orders, Redis answers "now", PostGIS answers "where", Socket.io delivers to exactly the right people. Then measure the latency you promise at the percentile your users actually feel.

Found this useful? Pass it on.

AK

Written by Adarsh Gupta

Full Stack Developer building admin panels · backend apis from Jaipur, India.

Say hi