SevyDevy

Guide · Frontend

Design a Ride-Hailing Map Frontend: Live Locations, Less Battery

28 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • A real time map frontend is the part of a ride app that shows a moving driver on a phone. It must spend little battery, data and processor time, because GPS, the mobile radio and redrawing are the costs your code controls.
  • On the driver's phone, a send filter drops most GPS readings, called fixes. It sends one only after real movement, such as 25 metres. A separate timer sends a heartbeat (a small 'still here' message) when 15 seconds pass without a send.
  • On the rider's phone, one open WebSocket (a connection both sides can send on) brings each update. The marker slides between updates, so one message every few seconds still looks smooth.
  • When the page is hidden, close the socket and stop timers. The Geolocation spec already pauses location updates for hidden pages, but you cannot count on the browser to close an open WebSocket.
  • Beginners can stop after the section on smooth markers. Engineers then get tested reconnect code that never glides the marker across the city and notices a link that has gone silent. They also learn why a page cannot send a WebSocket Ping (the protocol's built-in 'are you there' check).

What goes wrong when a live ride map updates every second?

The phones run hot and the batteries drain, because a real time map frontend that updates every second keeps the GPS, the mobile radio and the screen drawing busy. It does all that far more often than anyone can notice. On a weak network the marker also freezes and then jumps.

Picture a common setup. An auto-rickshaw and bike-taxi booking service in Lucknow has two web apps: a rider page for booking and a driver app on low-cost Android phones clipped to the handlebar. The setup is made up, but the problem is real.

The first version of its live map was simple. The driver app asked for GPS at the highest accuracy and sent every reading to the server, once a second. The rider's page redrew the whole map for every message.

It looked fine in the office demo. Then the team read a week of support tickets and rode autos around Hazratganj and Gomti Nagar. Three complaints kept coming back (the numbers are made up):

  • Drivers said their phones got hot by noon and were flat by 3 pm. Some turned the app off to save charge, so riders saw fewer autos.
  • Riders waiting eight minutes for a pickup lost 6 percent of their battery, just from the map screen.
  • On weak networks near Charbagh station, the auto marker froze, then jumped 200 metres in one step. Riders thought the driver had gone the wrong way.

None of these is a bug in the usual sense. Every line of code did what it was told. The design asked the phone to do too much, too often, for too little gain. The rest of this guide rebuilds it piece by piece.

How do you design a real-time map frontend?

You design a real time map frontend as two small pipelines. The driver's phone turns raw GPS readings into a few useful updates, and the rider's phone turns those updates into smooth movement on the map.

Two words help here. A fix is one GPS reading: a latitude, a longitude, an accuracy and a time. A location update is a fix that the phone decides is worth sending to the server.

Think of a shepherd on a hillside. He does not run after every sheep. He glances at the flock now and then, and walks over only when one strays far. A good live map works the same way.

The shepherd picture, mapped to the live map
ShepherdLive mapWhat it costs
A glance at the flockReading one GPS fixGPS power
Walking over to a sheepSending one location updateRadio power and data
Only when a sheep strays farOnly after real movement or a heartbeatSaves both

Where the picture breaks: a glance costs the shepherd nothing, but every fresh fix costs the phone battery. That is why the driver app also chooses how precise and how fresh each fix must be.

Here is the whole path. Start at the driver's GPS and follow the arrows to the rider's screen. Notice that the filter sits on the driver's phone, before any data touches the network.

The live ride map, end to end

Notice where the work happens: the driver's phone filters, the backend fans out, and the rider's page only moves one marker.

Diagram connections:

  • Driver phone GPS to Send filter
  • Send filter to Location service (Only useful fixes)
  • Location service to Live update gateway (Update for this ride)
  • Live update gateway to Rider's ride page (Pushed over a WebSocket)
  • Rider's ride page to Marker mover
  • Map tile CDN to Marker mover (Loaded once, then cached)
  • Driver phone GPS: Hands the app a stream of fixes
  • Send filter: Keeps only fixes worth sending
  • Location service: Stores each driver's latest position
  • Live update gateway: Holds one open connection per rider
  • Rider's ride page: Receives updates for one ride only
  • Marker mover: Slides only the driver marker
  • Map tile CDN: Serves the still base map, cached

This guide stays on the phones. Matching riders to drivers and storing millions of positions is a backend topic, covered in the ride-hailing system design guide.

On the phone, every design choice spends from three budgets. Keep them in mind for the rest of the guide.

  • The GPS budget. Asking for a precise location, often, keeps the location hardware working hard.
  • The radio budget. Every message wakes the phone's mobile radio, and a radio that keeps waking up rarely gets to rest.
  • The drawing budget. Every change on screen costs processor time, and redrawing the whole map costs far more than moving one marker.

How often should a ride-hailing driver app send its location?

A driver app should send a location when the driver has moved a meaningful distance, or when a heartbeat is due. A fixed one-second timer sends far more than the rider can use.

A heartbeat is a small message sent on a schedule, just to say the driver is still there. It matters when an auto waits at a red light and does not move at all.

Reading GPS in the browser

In a browser, the driver app reads GPS with navigator.geolocation.watchPosition. You pass it a function, and the browser calls that function each time the device's position changes. It returns a number, and passing that number to clearWatch stops the watching.

The call takes three options, and each one is a battery choice. The defaults below come from the W3C Geolocation spec, a Candidate Recommendation Snapshot dated 26 March 2026. The W3C is the main standards body for the web.

  • enableHighAccuracy, false by default. The spec warns that turning it on can mean slower responses or more power use.
  • maximumAge, 0 by default. A larger value, in milliseconds, lets the browser hand back a recent cached fix instead of fetching a new one.
  • timeout, effectively no limit by default. It sets how long, in milliseconds, the browser may take before calling your error function.

How often the browser calls you depends on the phone and the browser, not on your code. The real saving comes from deciding which fixes to send. Here is the send filter in TypeScript, which is JavaScript with type labels added.

The send filter

typescript
type Fix = { lat: number; lng: number; accuracy: number; time: number };

const MIN_METRES = 25;      // moved enough to matter
const MAX_GAP_MS = 15_000;  // heartbeat even when standing still
const MAX_ACCURACY_M = 100; // vaguer fixes are dropped

function metresBetween(a: Fix, b: Fix): number {
  const R = 6_371_000; // mean Earth radius in metres
  const toRad = Math.PI / 180;
  const dLat = (b.lat - a.lat) * toRad * R;
  const dLng = (b.lng - a.lng) * toRad * R * Math.cos(a.lat * toRad);
  return Math.hypot(dLat, dLng); // flat-ground shortcut, fine for short hops
}

function shouldSend(lastSent: Fix | null, next: Fix): boolean {
  if (next.accuracy > MAX_ACCURACY_M) return false;            // too vague to trust
  if (lastSent === null) return true;                          // first fix of the ride
  if (next.time - lastSent.time >= MAX_GAP_MS) return true;    // heartbeat is due
  return metresBetween(lastSent, next) >= MIN_METRES;          // moved enough
}

Three checks carry the design. The accuracy check drops fixes the phone itself calls vague. The heartbeat check sends a fix once 15 seconds have passed since the last send. It runs only when a fix arrives, though, and the next part deals with that.

The last check sends only after 25 metres of movement. The distance maths is a flat-ground shortcut. Against the exact formula for distance on a sphere (the haversine formula), it differed by under 0.01 percent up to 5 kilometres in our test at Lucknow's latitude.

One rule for the caller: pass the last fix you actually sent, not the last fix you saw. Otherwise an auto moving 5 metres between readings never sends again, because no two readings are 25 metres or 15 seconds apart.

text
fixes: 600, sent: 96, dropped: 504 (of which too vague: 12)
messages per hour at this rate: 576

What just happened? While moving, a send went out every 5 seconds, when the auto had covered 30 metres. At the signal, only the heartbeat fired, once every 15 seconds. The vague fixes never left the phone.

That is 96 messages instead of 600. At this mix of driving and waiting, an hour comes to about 576 messages instead of 3,600, for a similar picture on the rider's screen.

Why the heartbeat needs its own timer

The filter runs only when the browser hands over a new fix. The W3C spec lets the browser wait for a significant change in position, and leaves what counts as significant to each browser.

So an auto standing at a red light may produce no fixes at all, and then the filter sends no heartbeat. A small timer closes that gap.

typescript
function startTracking(send: (f: Fix) => void): () => void {
  let lastSent: Fix | null = null;
  const sendFix = (f: Fix) => { lastSent = f; send(f); };

  const watchId = navigator.geolocation.watchPosition(
    (p) => {
      const fix: Fix = { lat: p.coords.latitude, lng: p.coords.longitude,
                         accuracy: p.coords.accuracy, time: p.timestamp };
      if (shouldSend(lastSent, fix)) sendFix(fix);
    },
    (err) => console.warn('GPS error', err.code), // the timer below keeps running
    { enableHighAccuracy: true },
  );

  // A phone standing still may get no new fixes, so the heartbeat has its own timer.
  const timer = setInterval(() => {
    if (document.visibilityState === 'hidden') return; // no fixes then either, so stay quiet
    if (lastSent && Date.now() - lastSent.time >= MAX_GAP_MS) {
      sendFix({ ...lastSent, time: Date.now() }); // same place, fresh time: "still here"
    }
  }, 1000);

  return () => { navigator.geolocation.clearWatch(watchId); clearInterval(timer); };
}

startTracking starts the watch and returns a stop function for the end of the ride. The timer checks once a second, and resends the last position with a fresh time when 15 seconds pass without a send.

The resend proves the phone and the app are alive, not that the GPS has a new fix. So the timer stays quiet while the page is hidden, when the spec pauses fixes too. This version asks for high accuracy, as in the pickup phase.

What just happened in our test? Headless Chromium 141 (Chrome's open-source core, run without a window) was given a fake location that stood still. It delivered one fix in 40 seconds. The filter sent that fix at 0 seconds, and the timer resent it at 15 and 30 seconds. With the page faked as hidden for 32 seconds, it sent nothing.

We also replayed the 10-minute drive with no fixes during the 3-minute stop. The filter alone left a gap of 185 seconds with no message. With the timer, the longest gap was 15 seconds.

Follow the same logic as a picture. Every new fix enters at the top, and most of them leave through the drop box. The heartbeat timer feeds the same send box.

The send filter, one fix at a time

Notice that most fixes end in the drop box on the phone. Only the Yes path and the heartbeat timer cost a network message.

Diagram connections:

  • New GPS fix arrives to Accurate within 100 m?
  • Accurate within 100 m? to Drop the fix (No)
  • Accurate within 100 m? to Moved 25 m or 15 s passed? (Yes)
  • Moved 25 m or 15 s passed? to Drop the fix (No)
  • Moved 25 m or 15 s passed? to Send to location service (Yes)
  • Send to location service to Remember as last sent
  • Heartbeat timer to Send to location service (15 s with no send)
  • New GPS fix arrives: From watchPosition
  • Accurate within 100 m?: The fix reports its own accuracy
  • Moved 25 m or 15 s passed?: Distance rule or heartbeat rule
  • Drop the fix: Costs no radio time at all
  • Send to location service: About 100 bytes as JSON
  • Remember as last sent: The next fix is compared with this one
  • Heartbeat timer: Checks once a second, fix or no fix

Rules that change with the ride

One rule does not fit every moment of a ride. A driver who is idle needs few updates. A driver 200 metres from the pickup needs many, because the rider is staring at the screen.

Location rules by ride phase (a made-up starting point to tune)
Ride phaseHigh accuracySend ruleWhat the rider sees
Online, no ride yetOffEvery 200 m or 60 sNearby autos, refreshed every 10 s
Driving to pickupOnEvery 25 m or 15 sOne moving marker and an arrival time
Last 300 m to pickupOnEvery 10 m or 5 sMarker moving closely, ready to meet
On the tripOnEvery 25 m or 15 sRoute progress and time to drop
Driver app hiddenNot usedNone: the spec pauses updatesMarker greys out and shows its age

Should a live location map use WebSockets, SSE or polling?

For a ride screen, one long-lived connection usually fits best, most often a WebSocket. The server must push each driver update as it happens, and polling would ask again and again for nothing.

Polling means the page asks the server for news on a timer, say every two seconds. Most answers say nothing changed. Each empty question still wakes the radio and costs data.

A WebSocket is one connection that stays open, so both sides can send messages whenever they like. RFC 6455, the standard that defines it, uses a single TCP connection for traffic in both directions. TCP is the basic link between two machines on the internet.

Server-Sent Events, or SSE, is a one-way stream from server to browser over plain HTTP. A rider's page mostly listens, so SSE can work. One catch: MDN, Mozilla's web developer documentation, warns that without HTTP/2, a browser allows only 6 SSE connections per site, shared across all tabs. HTTP/2 is a newer version of HTTP that carries many requests over one connection.

This guide uses WebSockets because the driver app also sends, and one kind of connection is easier to run. The WebSockets vs SSE vs long polling guide compares the three in depth.

Why the page cannot send a Ping

Connections can die quietly, for example when a phone loses signal in an underpass or switches from Wi-Fi to mobile data. The other side may get no close message at all. RFC 6455 defines Ping and Pong frames for this, which are small control messages inside the connection. A Ping can serve as a keepalive, and the other side must answer with a Pong.

Here is the catch. The browser's WebSocket API has only two methods, send and close. The WHATWG is the group that maintains browser standards such as HTML. Its WebSockets Standard says Ping and Pong are not exposed to page code, as of 19 October 2026.

The browser still answers a server's Ping by itself. In our test, headless Chromium 141 replied to a small Node.js test server's Ping with a Pong. So the server can find dead riders, but the page cannot probe the server.

To let the page notice a dead link, have the server send a heartbeat, say every 10 seconds. The simplest heartbeat repeats the latest update with the same sequence number, the counter that rises with each update, so the page draws nothing new. If the page hears nothing at all for 30 seconds, it drops the socket and reconnects. The connection code later in this guide does both.

What goes inside each message

For battery, what goes inside each message matters as much as the pipe. Here is one update as JSON, the common text format for data, with ride id, sequence number, position, accuracy, heading (direction of travel), speed and time.

json
{"ride":"r_81723","seq":412,"lat":26.84671,"lng":80.94616,"acc":12,"hdg":274,"spd":6.1,"t":1760870412000}
  • That message is 105 bytes. Send only what the screen needs, and leave fare, driver name and photo to a separate request made once per ride.
  • Round coordinates to five decimal places. That is about 1.1 metres north to south, already finer than a fix whose accuracy is 10 metres or more.
  • Subscribe to one ride, not to the whole city. The gateway should send a rider only their own driver's updates.
  • Add a sequence number to each message, so the page can ignore one that arrives late and out of order.

How do you make map markers move smoothly between location updates?

Animate the marker from where it is on screen to the new position over a short time, instead of jumping it. The rider then sees steady movement, even when updates arrive only every few seconds.

The naive version sets the marker to each new position at once. With updates 5 seconds apart, the auto hops 30 metres at a time, and the rider reads every hop as a glitch.

There are two ways to fill the gaps. Interpolation slides the marker along a straight line between two points you already know. It never runs past the latest real fix, but it runs behind the true position by up to about one update gap, plus network delay.

Extrapolation guesses ahead from the last speed and direction. It feels more live, but it can overshoot. An auto that brakes hard at a crossing would keep sliding forward on screen, then snap back.

Interpolation has its own flaw: on a turn, the straight line cuts the corner. In our test, fixes 15 metres either side of a right-angle corner put the middle of the slide 7.5 metres from the centre of the road.

This design still picks interpolation. A marker that lags a little or cuts a corner bothers riders less than one that shoots ahead and snaps back.

The code uses requestAnimationFrame, a browser function that runs your callback just before the next screen repaint. MarkerMover remembers where the marker is on screen, so a new update starts from there.

typescript
type Point = { lat: number; lng: number };
type Marker = { setPosition(p: Point): void };

class MarkerMover {
  private shown: Point;   // where the marker is on screen right now
  private frameId = 0;

  constructor(private marker: Marker, start: Point) {
    this.shown = start;
    marker.setPosition(start);
  }

  moveTo(target: Point, durationMs: number): void {
    cancelAnimationFrame(this.frameId); // stop any slide still running
    const from = this.shown;            // start from the screen, not the last update
    const startTime = performance.now();
    const frame = (now: number) => {
      const t = Math.min((now - startTime) / durationMs, 1); // 0 at the start, 1 at the end
      this.shown = {
        lat: from.lat + (target.lat - from.lat) * t,
        lng: from.lng + (target.lng - from.lng) * t,
      };
      this.marker.setPosition(this.shown);
      if (t < 1) this.frameId = requestAnimationFrame(frame);
    };
    this.frameId = requestAnimationFrame(frame);
  }

  jumpTo(p: Point): void {
    cancelAnimationFrame(this.frameId); // no slide: used after a reconnect
    this.shown = p;
    this.marker.setPosition(p);
  }

  stop(): void {
    cancelAnimationFrame(this.frameId);
  }
}

The line with Math.min turns elapsed time into a fraction from 0 to 1, and stops at 1, so the marker never passes the target. Each frame, one redraw of the screen, moves the marker that fraction of the way.

What just happened in our test? An update arrived halfway through a 30 metre slide, when the marker was 15 metres along. The next slide started from those 15 metres, and the largest step between two frames was 0.15 metres.

A common shortcut starts each slide from the last update instead of from the screen. In the same test, that version jumped 15.1 metres in one frame, the very hop this section set out to remove.

Set durationMs to the expected gap until the next update. In the pickup phase that is about 5 seconds. jumpTo exists for one case, a reconnect, which the engineer sections cover. The constructor needs a start point, but any spot will do, because the first server message jumps the marker.

Two more details make the marker feel real. Rotate it to the heading in each message, so the auto points the way it is going. And let the backend snap positions to the road, because a raw fix can sit inside a shop. Snapping corrects the two end points, not the straight slide between them.

What should a live map do when the page is hidden?

When the page is hidden, close the live connection and stop every repeating job. When the rider comes back, reconnect and place the marker at the latest position at once, without a long slide.

The browser tells you about this through the Page Visibility API. The document fires a visibilitychange event, and document.visibilityState then reads visible or hidden. MDN says a page is hidden when its tab is in the background, its window is minimised, or the screen is off.

What the browser already slows down

Browsers cut some work on their own. MDN notes that most browsers pause requestAnimationFrame callbacks in background tabs. Timers such as setTimeout are throttled, which means the browser runs them less often than you asked.

Chrome goes further with what it calls intensive throttling, added in Chrome 88 in January 2021. When four conditions all hold, Chrome checks those timers only once per minute. Its post says this reduces CPU usage and so battery usage.

The four conditions: the page has been hidden for over 5 minutes, and the timer is part of a chain of 5 or more. The page has been silent for 30 seconds, and WebRTC (the browser's audio and video calling feature) is not in use.

The sources differ on WebSockets. MDN's Page Visibility page says tabs using WebSockets or WebRTC go unthrottled. Chrome's four conditions name WebRTC but not WebSockets. Either way, the Page Visibility API only reports the change; it never closes your socket. Chrome's Page Lifecycle article adds that browsers sometimes freeze or discard hidden tabs, at their own discretion.

Pause on hidden, catch up on return

The rule is simple. On hidden, close the socket and stop the animation. One product rule goes with it: the arrival alert must come from the backend, for example as a push notification (which needs the rider's permission), not from the open page.

On return, the page reconnects. The server's first message on every new connection is a snapshot of the latest position, so nothing missed while hidden matters. The diagram shows the loop.

The ride page when the rider looks away

Notice that the page holds no connection while hidden. The alert comes from the backend, and the first message after reconnecting places the marker.

Diagram connections:

  • Live ride screen to Page hidden?
  • Page hidden? to Keep sliding (No)
  • Page hidden? to Close socket, stop timers (Yes)
  • Close socket, stop timers to Backend sends arrival alert (Driver nears pickup)
  • Backend sends arrival alert to Rider opens the page
  • Rider opens the page to Reopen the socket
  • Reopen the socket to First message: snapshot
  • First message: snapshot to Live ride screen (Back to live updates)
  • Live ride screen: Socket open, marker sliding
  • Page hidden?: Checked on each visibilitychange
  • Keep sliding: Nothing changes while visible
  • Close socket, stop timers: The page does no repeating work
  • Backend sends arrival alert: A push notification, not the open page
  • Rider opens the page: Taps the alert or unlocks the phone
  • Reopen the socket: Subscribe to this ride again
  • First message: snapshot: Latest position, placed without a slide

Here is the connection code, using MarkerMover from the previous section. sync opens or pauses the link to match the page's visibility. armSilenceTimer and retryLater deal with a link that goes quiet or drops.

typescript
type Update = { seq: number; lat: number; lng: number };

class LiveConnection {
  private socket: WebSocket | null = null;
  private lastSeq = 0;
  private attempt = 0;
  private retryTimer: ReturnType<typeof setTimeout> | undefined;
  private silenceTimer: ReturnType<typeof setTimeout> | undefined;
  private needsJump = true;

  constructor(private url: string, private mover: MarkerMover) {
    document.addEventListener('visibilitychange', () => this.sync());
    this.sync();
  }

  private sync(): void {
    if (document.visibilityState === 'hidden') this.pause();
    else this.connect();
  }

  private pause(): void {
    clearTimeout(this.retryTimer);
    clearTimeout(this.silenceTimer);
    this.mover.stop();
    const s = this.socket;
    this.socket = null; // cleared first, so onclose knows we closed it
    s?.close(1000, 'page hidden');
  }

  private connect(): void {
    if (this.socket) return; // already live, so calling twice is safe
    const s = new WebSocket(this.url);
    this.socket = s;
    this.needsJump = true;   // the server's first message is a fresh snapshot
    this.armSilenceTimer();
    s.onmessage = (e) => {
      this.attempt = 0;      // a real message arrived, so the link works
      this.armSilenceTimer(); // any message, heartbeat included, proves the link is alive
      const u: Update = JSON.parse(e.data);
      const isNew = this.needsJump ? u.seq >= this.lastSeq : u.seq > this.lastSeq;
      if (!isNew) return;    // late, repeated or a heartbeat: nothing to draw
      this.lastSeq = u.seq;
      if (this.needsJump) this.mover.jumpTo(u); // place it, do not slide
      else this.mover.moveTo(u, 5000);          // pickup phase: about 5 s apart
      this.needsJump = false;
    };
    s.onclose = () => {
      if (this.socket === s) this.retryLater(); // a drop, not a close we asked for
    };
  }

  private armSilenceTimer(): void {
    clearTimeout(this.silenceTimer);
    this.silenceTimer = setTimeout(() => this.retryLater(), 30_000); // 30 s of silence
  }

  private retryLater(): void {
    clearTimeout(this.silenceTimer);
    const s = this.socket;
    this.socket = null; // forget it first, so its onclose is ignored
    s?.close();
    const waitMs = Math.min(30_000, 1000 * 2 ** this.attempt++); // 1, 2, 4 ... 30 s
    this.retryTimer = setTimeout(() => this.sync(), waitMs + Math.random() * 1000);
  }
}

What just happened? We ran this class in headless Chromium 141 against a local server that sends one update a second. The first message placed the marker with a jump, and every later one slid it.

  • A late message with an old sequence number was ignored.
  • Calling sync twice while visible still opened only one connection.
  • Faking a hidden page closed the socket with code 1000, the normal close, and no messages arrived for 5 seconds.
  • Showing the page again reconnected, and the snapshot placed the marker at once.
  • When the server dropped the connection, the page reconnected between 1 and 2 seconds later: the 1 second wait plus the random extra.
  • For 35 seconds the server sent only heartbeats, plus one message with no sequence number. The marker stayed put, and no position became invalid.
  • When the server kept the socket open but went silent, the page dropped it 30 seconds after the last message. It reconnected 1.7 seconds later.
  • The first message on that new connection repeated the last sequence number, and it still placed the marker.

Three details are easy to miss. The socket field is cleared before close, so onclose can tell a deliberate close from a dropped one. The first message after connecting may repeat the last sequence number, so it accepts an equal one. And jumpTo exists because sliding from a five-minute-old spot would glide the auto across half of Lucknow.

How do you handle weak networks and stale locations on a live map?

Treat every location as a guess with an age and an accuracy, and show both honestly. Fade a stale marker, draw an accuracy circle when it is large, and reconnect with growing waits.

Each fix comes with an accuracy value. The W3C spec defines it as a radius in metres at a 95 percent confidence level. So an accuracy of 40 means the true spot is very likely within 40 metres of the dot.

The screen should say how old its knowledge is. These are starting thresholds to tune, not standards:

  • Under 20 seconds old: the marker looks normal and keeps sliding.
  • 20 to 60 seconds old: a small label says when the location was last updated.
  • Over 60 seconds old: the marker turns grey, and the arrival time says it is an estimate.
  • Accuracy worse than 50 metres: a soft circle shows the area where the auto probably is.

Reconnecting needs care too. If a phone loses signal and retries every second, it burns battery and floods the server the moment the network returns.

The class waits 1 second, then 2, 4, 8 and 16, then 30 seconds for every later try, plus up to 1 second of random extra. The wait resets only when a message arrives, not when a connection merely opens, so a server that accepts connections but sends nothing cannot cause fast retries.

That pattern is exponential backoff with jitter, and it has its own guide. The random extra spreads out thousands of phones that lost signal at the same moment, such as riders leaving a crowded station.

Messages sent while the phone was offline are gone, and that is fine. The snapshot that opens every new connection fills the gap in one message.

How do you keep a live map fast on cheap Android phones?

Keep the base map still and cached, and change only the few markers that move. Most of the screen should cost nothing after the first load.

A map is drawn from tiles: small square images or data pieces, each covering one patch of the city at one zoom level. Roads do not change minute to minute, so tiles can come from a CDN, a network of servers close to users, and stay in the browser cache.

The team tested on a Rs 8,000 Android phone, not on laptops. These are the rules it kept:

  • Draw driver markers on one layer above the base map, so moving a marker never redraws the streets.
  • On the home screen, show at most 20 nearby autos, and refresh them every 10 seconds, not live.
  • When several messages arrive within one frame, apply only the newest one for each driver.
  • Keep slow-changing ride details, such as fare and driver name, apart from fast-changing marker positions.
  • Stop following the driver if the rider pans the map by hand, and offer a button to re-centre.

How do you explain a real-time map frontend in a system design interview?

Explain it as two pipelines with three budgets, GPS, radio and drawing, and justify each choice by the budget it protects. Then walk through these steps.

  1. Clarify the screens

    Ask which screens show live data: nearby drivers, the pickup wait or the trip. Ask whether the driver app is native or web, since that decides background tracking.

  2. Draw the pipeline

    Driver GPS, a send filter, the location service, a live gateway, the rider's page and the marker mover. Keep backend boxes brief.

  3. Set the send rules

    Explain the distance rule, the heartbeat timer, the accuracy check, and how the rules change by ride phase. Quote the numbers: 96 of 600 fixes sent.

  4. Choose the connection

    Pick a WebSocket, or SSE for listen-only pages. Say the page cannot send Pings, so the server sends heartbeats and the page reconnects after 30 seconds of silence. Keep messages near 100 bytes and scoped to one ride.

  5. Make movement smooth

    Interpolate from the marker's on-screen spot with requestAnimationFrame, and update the marker outside the framework's render cycle.

  6. Handle hidden pages and bad networks

    Close on hidden, snapshot as the first message, back off on reconnect, show stale data honestly, and ignore out-of-order messages.

Questions people ask

Does watchPosition drain the battery?
It can, depending on how you call it. The W3C spec warns that high accuracy can mean more power use. Turn it on only for phases that need it, and stop the watch and the heartbeat timer when the ride ends. Filter fixes before sending them, so the radio stays quiet too.
How often should a rider's map update?
Often enough that the marker never looks stuck, which usually means every few seconds during pickup. With the marker sliding between updates, a gap of 5 to 15 seconds usually still looks like steady movement. Send more often in the last few hundred metres, when the rider is watching closely.
Can a web app track location in the background?
Not with the standard Geolocation API. The W3C spec, as of its 26 March 2026 snapshot, sends position updates only to visible pages. A browser-based driver app therefore needs to stay on screen during a ride. Apps that must track in the background are usually built as native apps.
Is SSE good enough for a live location map?
Yes, for a page that only listens. Server-Sent Events give a one-way stream from server to browser over plain HTTP. Without HTTP/2, MDN warns of a limit of 6 connections per site across all tabs. Choose WebSockets when the same app also sends, or to run one kind of connection for drivers and riders.
Why does my map marker jump instead of moving smoothly?
It jumps because the page sets each new position at once, and updates arrive seconds apart. Slide from the marker's current on-screen spot to the new one with requestAnimationFrame, over roughly the gap until the next update. Starting from the last update instead of the screen causes a smaller jump of its own.
Can browser JavaScript send a WebSocket Ping?
No. The browser WebSocket API has only two methods, send and close, and the WHATWG standard says Ping and Pong are not exposed to pages. The browser does answer a server's Ping by itself. To detect a dead link from the page, have the server send small heartbeat messages and reconnect after a silence.
Should a live map close its WebSocket when the tab is hidden?
Usually yes, for a rider's screen. You cannot count on the browser to close it, and MDN says tabs using WebSockets go unthrottled in the background. Close it on hidden, send the arrival alert from the backend, and let the first message after reconnecting be a snapshot.

Sources and further reading

Practise this with Lala

Practise