A production-grade, horizontally scalable real-time location tracking system built with Node.js, TypeScript, Express, Socket.IO, and Leaflet.
Designed to handle high-frequency telemetry data, this system demonstrates advanced distributed backend patterns — token-bucket rate limiting, spatial partitioning for
In logistics, ride-sharing, and delivery networks, ingesting live telemetry isn't just about rendering dots on a map — it's about managing chaos. Mobile networks drop connections, GPS chips teleport across cities due to signal bounce, and compromised clients can spam servers with thousands of updates per second.
This project moves beyond a basic WebSocket tutorial by tackling real-world constraints: enforcing ordered chronology, defending against backpressure, optimizing spatial mathematics, and guaranteeing graceful state failovers — all in strict TypeScript end-to-end.
- System Design Thinking: Actively trades strict consistency for high availability during cache failures, demonstrating applied CAP theorem principles.
-
Algorithmic Optimization: Dropped geofence complexity from
$O(M)$ to$O(1)$ by mapping fences into a 2D spatial grid. - Defensive Engineering: Token-buckets, velocity-based drift filtering, and bounded queues protect V8 from OOM crashes and event-loop blocking.
-
Type Safety: Full TypeScript migration across backend (
src/) and frontend (public/src/) with zeroanyleaks in interfaces.
- Multi-Socket Persistent Identity: Maps JWT identity to a
Set<SocketId>, natively handling tab duplication and preventing zombie drops during cell-tower handoffs. - Graceful State Failover: Storage layer starts on Redis (
MULTI/EXECatomic blocks) but degrades transparently to in-memory Maps if the cache node becomes unreachable. - Telemetry Downsampling & Bounded Queues: Samples locations (>5s intervals) and rigidly enforces history length to prevent V8 OOM crashes.
- 60FPS Rendering:
requestAnimationFrameinterpolation and marker clustering maintain smooth rendering under high-frequency data streams. - Follow Mode: Click any user to lock the camera via continuous smooth-panning (
map.panTo), with a glassmorphic panel showing live Haversine distance, idle/active state, and last ping delta. - Distance-Aware Fleet Sidebar: Dynamically recalculates host-to-fleet distances on every tick, sorting the active user list with nearest entities first.
- Live Activity Feed: Floating ephemeral toasts for geofence breaches, joins, and disconnects — non-blocking, auto-expiring.
The system is a layered monolith ready for microservice extraction.
- Transport Layer (
server.ts,locationHandler.ts) — WebSocket upgrade, JWT validation, boundary exception handling. - Service Layer (
locationService.ts,geofenceService.ts) — Pure business logic, rate limiting, spatial math. Fully decoupled from sockets. - Storage Layer (
userStore.ts) — Abstracted interface wrapping bothRedisUserStoreandMemoryUserStore.
Each device update passes a synchronous pipeline before broadcast:
- Validation — Strict payload schema check.
- Backpressure — Token Bucket drops spam instantly ($O(1)$) before touching the event loop.
- Chronology Check — Monotonic timestamp ordering prevents out-of-order delivery.
- Velocity Drift Filter — Haversine distance ÷ Δtime; rejects GPS noise above physical speed limits.
- State Update — Persists sanitized location with downsampling.
-
Geofence Evaluation —
$O(1)$ spatial grid cell lookup. - Broadcast — Pushes finalized payload to the scoped Socket.IO room.
| Decision | Advantage | Tradeoff | Mitigation |
|---|---|---|---|
In-Memory Grid vs. Redis GEORADIUS |
Zero network latency (~0.01ms) | Requires Pub/Sub to sync new fences globally | Reads outnumber writes |
| Local Token Bucket vs. Redis Lua | No network hop per frame | Relies on sticky sessions for strict accuracy | Accept eventual consistency |
| Memory Fallback vs. 503 Outage | High availability | Sacrifices global consistency across nodes | Alerting + regional isolation |
| Room-Based Pub/Sub vs. Global Broadcast | Caps CPU fan-out | Users must subscribe to regions explicitly | Default room handles unassigned clients |
- Geofence Grid Boundaries: Fences spanning multiple cells are mapped to all relevant cell sets. Exit/enter detection evaluates both the current cell's candidates and any fence the user previously occupied.
- Redis Split-Brain: On failure, the system diverges to local memory. No automatic reconciliation on recovery — requires cold restart or a background rehydration worker.
- 100 users — Single process, <100MB RAM, grid checks <0.1ms.
- 10,000 users — Broadcast fan-out (CPU) becomes the bottleneck. The 5m movement filter drops ~80% of idle updates. Room scoping limits blast radius.
- 100,000+ users — Multi-node: HAProxy sticky sessions → 15+ Node.js instances → Redis Cluster for state and Pub/Sub. At this tier, Redis Pub/Sub must be replaced with partitioned message brokers (Kafka/NATS).
- JSON Serialization:
JSON.stringifyon full user state at 10,000 req/sec can block the single-threaded event loop. - Sticky Session Fragility: In-memory rate limiting and geofence occupancy break if the load balancer shifts a user to a different node.
- Split-Brain State: Nodes degraded to memory can't see each other's users.
- Redis
HSET: Replace full JSON overwrites with partial field mutations to reduce serialization pressure. - Kafka / Time-Series: Migrate append-only trail history to a Kafka firehose → TimescaleDB.
- Geohash Indexing: Replace the uniform grid with Geohash indices for variable-size fence support.
- Node.js v18+
- Redis (optional — required only for multi-node distributed mode)
git clone https://github.com/yourusername/RealTimeDeviceTrack.git
cd RealTimeDeviceTrack
npm installNODE_ENV=development
PORT=8000
JWT_SECRET=your_secret_here
# Location filtering
LOCATION_THROTTLE_MS=2000
MAX_SPEED_MPS=40
LOCATION_RATE_LIMIT_WINDOW_MS=10000
LOCATION_RATE_LIMIT_MAX=10
# Optional: Redis for distributed mode
REDIS_URL=redis://localhost:6379npm run dev # Backend + Vite dev server (frontend TS with HMR)
npm run build # Compile backend and bundle frontend to dist/public/
npm start # Run compiled production build (node dist/server.js)
npm run typecheck # Type-check both configs, no emit
npm run clean # Wipe dist/During development, open the app at http://localhost:5173 (Vite). The backend API and Socket.IO traffic are proxied to http://localhost:8000.