Fleet EcosystemClient project

Real-Time Fleet Tracking in Fleeticc with SSE and AWS

Fleet operators need a map that moves when the van moves, not after the next refresh. This case study covers the SSE pipeline we built for Fleeticc on AWS and the approaches we ruled out along the way.

NestJSNext.jsGoPostgreSQLTimescaleDBRedisAWS
Build-That Team5 min read
Real-Time Fleet Tracking in Fleeticc with SSE and AWS

How we replaced polling with Server-Sent Events on NestJS, why WebSockets were the wrong fit, and how the pipeline runs on AWS

When we started Fleeticc, the first product question was not which map library to pick. It was how a dispatcher sees a vehicle move without refreshing the page. GPS hardware already pushes coordinates every few seconds. The hard part is getting those updates to the web dashboard and mobile apps reliably, at scale, without burning server resources on empty polling loops.

We evaluated three patterns: HTTP polling, WebSockets, and Server-Sent Events (SSE). Polling was ruled out on day one. WebSockets looked attractive until we mapped them onto our AWS load-balancer setup and our mostly one-way data flow. SSE won, and this post explains how we wired it through NestJS, Redis, and the Fleeticc ingest layer on AWS.


The problem with polling

The naive approach, hitting a REST endpoint every five seconds and repainting markers, fails in production for three reasons. Latency is bounded below by the poll interval, so a vehicle turning a corner appears to teleport. Cost scales with connected users times poll frequency, even when nothing moved. Under load, polling creates thundering herds that compete with the same APIs handling trip history, reports, and geofence checks.

  • 5-second polling gives an average of 2.5 seconds of stale positions on the map
  • 50 dispatchers at 12 requests per minute equals 600 requests per minute just to detect zero changes
  • Battery drain on mobile from background timers waking the radio repeatedly
  • No back-pressure: clients keep hammering even when the fleet is parked overnight

Why we did not choose WebSockets

WebSockets are the default answer in many real-time tutorials. For Fleeticc they were the wrong tool. Our dashboard traffic is overwhelmingly server to client: a new latitude, a geofence alert, a device status flag. Clients rarely push coordinates back over the same channel because devices talk to the Go ingest service directly.

  • Bidirectional channels we did not need, adding protocol complexity for no gain
  • Sticky sessions or custom connection routing behind AWS ALB, harder ops than plain HTTP
  • Reconnect and heartbeat logic duplicated across web (EventSource) and mobile (would need a WS client)
  • Binary framing and custom message schemas; SSE sends plain text events the browser parses natively

WebSockets shine when both sides send high-frequency messages: chat, collaborative editing, multiplayer games. Fleet tracking is a broadcast problem. SSE is HTTP, works through standard proxies, and the browser reconnects automatically with Last-Event-ID when a tab sleeps.


SSE vs polling vs WebSocket at a glance

  • Polling: simple to build, unacceptable latency, worst cost profile at scale
  • WebSocket: lowest latency for bidirectional apps; overkill and harder to operate for read-heavy maps
  • SSE: one-way push over HTTP/2, native browser support, auto-reconnect, fits NestJS Observables cleanly

Architecture on AWS

GPS devices send TCP/UDP payloads to a Go ingest service on AWS ECS. Each fix is validated, deduplicated, written to TimescaleDB for history, and published to Redis Pub/Sub keyed by fleet ID. NestJS API tasks subscribe to those channels and fan out to open SSE connections. The Next.js dashboard opens an EventSource per fleet view. Flutter uses the same endpoint with a lightweight SSE client.

  • AWS ECS (Fargate): Go ingest and NestJS API, autoscaling on CPU and connection count
  • Amazon RDS (PostgreSQL + TimescaleDB): trip history, geofences, fleet metadata
  • Amazon ElastiCache (Redis): pub/sub bus between ingest and SSE workers, plus session cache
  • Application Load Balancer: idle timeout tuned for long-lived SSE connections
  • Amazon S3 and CloudFront: static assets, report exports, marketing site media
  • Amazon SES and Firebase FCM: email alerts and mobile push for events SSE does not need to carry

NestJS SSE endpoint (pattern)

The NestJS layer exposes one SSE route per fleet. Internally it returns an RxJS Observable that emits MessageEvent objects whenever Redis delivers a position patch. Auth runs before the stream opens, so anonymous clients never hold a connection.

typescript
@Sse('fleets/:fleetId/live')
@UseGuards(FleetAccessGuard)
livePositions(
  @Param('fleetId') fleetId: string,
  @Req() req: AuthenticatedRequest,
): Observable<MessageEvent> {
  return this.fleetStreamService.stream(fleetId, req.user.id);
}

// fleet-stream.service.ts: Redis message to SSE event
stream(fleetId: string, userId: string): Observable<MessageEvent> {
  return new Observable((subscriber) => {
    const channel = `fleet:${fleetId}:positions`;
    const onMessage = (_ch: string, payload: string) => {
      subscriber.next({ data: payload, type: 'position' });
    };
    this.redis.subscribe(channel, onMessage);
    return () => this.redis.unsubscribe(channel, onMessage);
  });
}

Next.js dashboard client

The Fleeticc web dashboard uses the browser EventSource API. On each message we merge the patch into the map layer with no full page reload and no polling timer. When the tab goes to the background, the connection drops gracefully. On focus, EventSource reconnects and we fetch a snapshot REST call to fill any gap.

typescript
useEffect(() => {
  const source = new EventSource(`/api/fleets/${fleetId}/live`, {
    withCredentials: true,
  });

  source.addEventListener('position', (event) => {
    const update = JSON.parse(event.data) as VehiclePosition;
    mergeVehicleOnMap(update);
  });

  source.onerror = () => {
    setConnectionState('reconnecting');
  };

  return () => source.close();
}, [fleetId]);

What shipped on the real-time layer

  • Live fleet map on web and mobile from the same SSE stream and Redis bus
  • Sub-second position latency without polling loops
  • AWS infrastructure sized for ingest spikes and long-lived dashboard tabs
  • Geofence and alert events pushed over the same SSE channel with typed event names

Fleeticc is live at fleeticc.com with iOS and Android apps on the same backend. For IoT and fleet products, match the transport to the data direction and put the cloud layer behind a pub/sub fan-out. That is what keeps the map honest when vehicles are actually moving.

Production case study

Fleeticc

Sri Lanka's locally built fleet tracking platform — fleeticc.com, web dashboard, and iOS/Android apps on one account for live GPS, alerts, and reports.

Next.jsFlutterGoNestJS
View project
ShareLinkedIn
FleeticcSSENestJSAWSReal-time

Project Inquiry

Let's do great
work together

Tell us about your project, whether it is a mobile app, web platform, or MVP, and we'll respond within 24 hours.

Ahmed Anwer, Founder of Build-That

Connect with Founder · Ahmed Anwer

Your name
Email address
Project details...