Skip to main content
Mobile App

Building a Real-Time Taxi Booking App with Live Maps

By Purandhar Sri Sai

A React Native taxi booking app with live driver tracking on the map. Replacing 5-second polling with WebSockets cut location delay from 4-5 seconds to under one, and API traffic by roughly 70%.

Platform

iOS & Android

Duration

7 weeks

<1s

Location delay

70%

Fewer API calls

25%

Battery savings

Project overview

We rebuilt the location layer around a persistent WebSocket connection instead of a polling loop, then optimized the map rendering and the GPS sampling around it. The result is a real-time taxi booking app where the marker tracks the driver closely enough to be worth looking at.

Location delay dropped from four or five seconds to under one. API requests fell by roughly 70%. Battery consumption came down about 25%, and visible map lag went to zero.

The app ships on iOS and Android from a single React Native codebase. A Node.js service holds the socket connections and fans position updates out to the riders subscribed to each ride. Google Maps supplies the map through react-native-maps, the Directions API supplies routes and arrival estimates, and the platform geolocation APIs supply the driver's position. Built in seven weeks.

None of that is exotic. Every piece is a documented API used roughly the way its documentation suggests. What made the difference was choosing the right transport for the data first, and then refusing to re-render or re-request anything that had not changed.

Platform

iOS & Android

Duration

7 weeks

Type

Mobile App

Stack

6 technologies

The challenge

Most taxi apps rely on continuous location updates and map rendering, which introduces serious real-world challenges. The first version of this one used REST polling: the rider's app asked the server for the driver's position every five seconds, and the server answered whether or not anything had changed.

On paper that is simple. On a phone it was visibly broken. Driver movement appeared jumpy, because a marker that updates once every five seconds jumps between positions rather than travelling along a road. Location delay ran to four or five seconds, so the car drawn on the map was consistently behind the car in the street. And the app made roughly sixty API requests per minute per user, the overwhelming majority of which returned a position the rider had already seen.

That cost compounds in three directions at once. Network traffic scales with the number of active riders rather than with how much anyone is actually moving, so a busy hour costs the same whether the fleet is driving or parked. The map re-rendered on every update, and redrawing the full marker set on a schedule made the whole interface sluggish to touch. Continuous GPS tracking at full accuracy kept the radio and the location subsystem working without pause, and drained the battery fast enough that riders noticed.

The real damage was to trust. A rider watching a taxi app is checking one thing: where is my driver right now. A four to five second lag answers a question about the recent past, and a rider who catches the map being wrong stops believing the arrival estimate next to it. Every problem below is a version of that one.

Continuous REST polling causes high network usage and visible lag

Non-stop map re-renders make the UI slow and unresponsive

Continuous GPS tracking drains battery quickly

4–5 second location delays destroy user trust

What we set out to do

  • 01

    Implement smooth real-time driver tracking

  • 02

    Reduce location delay to under 1 second

  • 03

    Optimize map rendering performance

  • 04

    Minimize battery and network usage

  • 05

    Build a scalable architecture for real-time updates

How we solved it

01

Map integration strategy

The map is the product surface, so the SDK choice constrains everything drawn on top of it. We evaluated react-native-maps with Google Maps against alternatives like Mapbox.

Google Maps won on three practical grounds: faster setup, better documentation, and stable performance for standard taxi use cases. None of those are glamorous, and that was the point. This app needs a map, a moving marker, and a route line. It does not need custom tile styling or offline vector rendering, and picking the SDK with the deeper feature set would have meant paying integration cost for capability the product never uses.

react-native-maps renders through the platform's native map view rather than wrapping a web map, so markers and polylines are drawn by the native SDK on both iOS and Android. That mattered later. By the time the performance work in step 04 started, marker drawing was already native, which meant the remaining cost sat in React re-rendering the component tree rather than in the map itself.

The decision also settled the routing provider. Displaying Google's map while routing against a different road graph would mean reconciling two sets of geometry, so the Directions API integration in step 03 followed from this choice rather than being made independently.

Key decision

Google Maps over Mapbox

Result

Stable baseline with native marker and polyline support

02

Switching from polling to WebSockets

Polling every five seconds sets a floor on latency that no amount of tuning gets under. The rider's app cannot learn about a position change until the next scheduled request, so average staleness is half the interval and the worst case is the whole of it. The measured four to five second delay was the floor working as designed, not a bug in the implementation.

Shortening the interval does not fix it, it just moves the cost. Halving the interval halves the delay and doubles the request count, and the request count was already the problem: roughly sixty per minute per user, almost all returning a position that had not meaningfully changed.

We replaced the loop with a persistent WebSocket connection handled by a Node.js service. The driver's app pushes position updates as they happen, and the server fans them out to the riders subscribed to that ride. Communication is bidirectional over the same connection, so ride state travelling the other way does not need a second mechanism.

The inversion is the whole point. Polling asks 'has anything changed' on a schedule. A socket says 'this changed' when it does. Redundant requests were not reduced here, they stopped existing, because there is no longer a request that can be redundant.

Key decision

WebSockets over REST polling

Result

Location delay dropped from ~5s to <1s. API calls cut from ~60/min to ~15/min.

03

Route rendering and ETA calculation

Tracking the driver's position answers where they are. Riders also want to know when the car arrives, and that needs a route.

We integrated the Google Directions API to fetch the optimal route between the driver and the pickup point, drew it on the map as a polyline, and updated it as the driver moved. The arrival estimate comes out of the same response, so the line on the map and the number beside it always describe the same route.

Updating that route naively is expensive. Re-fetching directions on every position update would have reintroduced exactly the request volume step 02 removed, on a more costly endpoint. So route re-fetching runs on a threshold rather than a timer. While the driver is following the route already drawn, the existing result stays valid and is served from cache. A new call goes out when the driver has deviated enough that the drawn route no longer describes where they are going.

That is the difference between treating directions as a subscription and treating them as a fact with a defined invalidation condition. The polyline redraws continuously from data already on the device, and the network call happens only when the underlying answer has actually changed.

Key decision

Threshold-based route re-fetch + caching

Result

Significant reduction in Directions API calls

04

Map performance optimization

With updates arriving in under a second, the render path became the bottleneck. Every incoming position triggered a state update, and every state update re-rendered the marker set, including every marker that had not moved.

All marker components are memoized, so a position update re-renders the marker it describes and leaves the rest alone. On a map showing a driver, a pickup point, a destination, and a route, that turns a full re-render into a single component update.

Location updates are also throttled to every one to two seconds rather than being applied continuously. That reads like a step back from a sub-second socket, and it is not. The socket removed the latency floor; throttling controls how often that fresh data is committed to React state. The device receives updates as they arrive and redraws on a controlled cadence, which is what keeps marker movement smooth rather than stepped.

The combination is what produces the result. Sub-second data with unthrottled rendering was fast and janky. Sub-second data with memoized markers and a throttled commit is fast and smooth: 60fps marker animation, no visible lag.

Key decision

Memoization + throttled updates

Result

No visible UI lag. Smooth 60fps marker animation.

05

Battery optimization

Continuous GPS at full accuracy is the most expensive thing a location app does, and for most of a ride it is unnecessary. A rider waiting on a driver who is eight minutes out does not need metre-level precision refreshed continuously. A rider being picked up right now does.

So tracking adapts to ride state. GPS accuracy drops when the app is idle, and the tracking interval scales with what is happening: coarser and less frequent when nothing is imminent, tighter as the driver closes in and during the ride itself. The device spends its power budget on the parts of a ride where precision changes what the rider sees.

This is step 03's principle applied to the sensor instead of the network. Both replace a fixed schedule with a condition, and in both cases the condition is whether the thing being tracked has changed in a way that matters. Estimated battery reduction is 20 to 30%.

Battery is also why the polling architecture was unsalvageable. Polling keeps the radio active on a timer regardless of ride state, so no amount of GPS tuning would have helped while the network layer was waking the device sixty times a minute anyway.

Key decision

Adaptive GPS intervals based on ride state

Result

Estimated 20–30% reduction in battery usage

Measurable impact

<1s

Location update delay (down from ~5s)

70%

Reduction in API requests

25%

Battery consumption reduction

0

Visible map lag

Tech stack

RReact NativeNNode.jsWWebSocketsGGoogle Maps APIRreact-native-mapsGGeolocation APIs

What we learned

This project surfaced the real challenges of building location-based, real-time systems in mobile applications, and most of them turned out to be about restraint rather than capability.

The transport decision was the one that mattered. Polling asks a question on a timer; a socket delivers an answer when there is one. Once the connection was persistent, the delay budget stopped being set by the polling interval and started being set by how fast the driver's device could report, which is where it should have been from the start.

Everything after that was about not doing work. Not re-rendering markers that had not moved. Not re-fetching a route that was still valid. Not sampling GPS at full accuracy while the app sat idle. Each one is a small decision, and together they account for the battery and API numbers more than any single optimization does.

The pattern generalizes past taxis. Anything that tracks a moving object on a map pays the same three costs: the transport, the render, and the sensor. Get the transport right first, because the other two are only worth optimizing once the data arriving is actually fresh.

  • 01

    Real-time is a property of the transport, not of the interval. Shortening a polling loop trades battery and bandwidth for latency; a persistent socket removes the trade instead of rebalancing it.

  • 02

    Map performance is a render problem before it is a map problem. Memoizing markers so only the one that moved re-renders is what buys 60fps, not anything inside the map SDK.

  • 03

    Fetch a route on a threshold, not on a timer. A Directions result stays valid for as long as the driver follows it, so the only honest trigger for a re-fetch is deviation from the route already drawn.

  • 04

    GPS accuracy is a dial, not a switch. Sampling rate and precision should scale with ride state, because an idle app does not need what an app mid-pickup needs.

  • 05

    Battery optimization is non-negotiable for any location-based mobile app. It is not a polish task at the end, it constrains the architecture from the first decision onward.

Frequently asked questions

Why use WebSockets instead of REST polling for live driver tracking?

Polling sets a latency floor that no amount of tuning gets under. At a five-second interval, average staleness is half the interval and the worst case is the whole of it, which is exactly the four to five second delay we measured. That was the floor working as designed, not a bug.

Shortening the interval doesn't fix it, it moves the cost. Halving the delay doubles the request count, and the request count was already the problem: roughly 60 requests per minute per user, almost all returning a position that hadn't meaningfully changed.

A persistent WebSocket connection inverts the question. Polling asks whether anything changed, on a schedule. A socket says that something changed, when it does. A Node.js service holds the connections and fans driver position updates out to the riders subscribed to that ride. Location delay dropped to under one second, and API traffic fell from roughly 60 requests per minute to about 15.

Which map API did you use, and why Google Maps over Mapbox?

react-native-maps with Google Maps. We evaluated it against Mapbox and chose Google on three practical grounds: faster setup, better documentation, and stable performance for standard taxi use cases.

This app needs a map, a moving marker, and a route line. It doesn't need custom tile styling or offline vector rendering, so picking the SDK with the deeper feature set would have meant paying integration cost for capability the product never uses.

react-native-maps also renders through the platform's native map view rather than wrapping a web map, so markers and polylines are drawn by the native SDK on both iOS and Android. That mattered later: by the time the performance work started, marker drawing was already native, so the remaining cost sat in React re-rendering the component tree rather than in the map itself.

The choice settled the routing provider too. Displaying Google's map while routing against a different road graph would mean reconciling two sets of geometry.

How is the arrival estimate calculated?

The Google Directions API returns the route between the driver and the pickup point. We draw it on the map as a polyline, and the arrival estimate comes out of the same response, so the line on the map and the number beside it always describe the same route.

Updating that route naively is expensive. Re-fetching directions on every position update would have reintroduced exactly the request volume the WebSocket change removed, on a more costly endpoint. So route re-fetching runs on a threshold rather than a timer. While the driver is following the route already drawn, the existing result stays valid and is served from cache. A new call goes out when the driver has deviated enough that the drawn route no longer describes where they're going.

That's the difference between treating directions as a subscription and treating them as a fact with a defined invalidation condition. The polyline redraws continuously from data already on the device, and the network call happens only when the underlying answer has actually changed.

How do you stop continuous GPS tracking from draining the battery?

Tracking adapts to ride state instead of running at a fixed rate. GPS accuracy drops when the app is idle, and the tracking interval scales with what's happening: coarser and less frequent when nothing is imminent, tighter as the driver closes in and during the ride itself.

A rider waiting on a driver who is eight minutes out doesn't need metre-level precision refreshed continuously. A rider being picked up right now does. The device spends its power budget on the parts of a ride where precision changes what the rider sees. Estimated battery reduction is 20 to 30%.

Battery is also why the polling architecture was unsalvageable. Polling keeps the radio active on a timer regardless of ride state, so no amount of GPS tuning would have helped while the network layer was waking the device 60 times a minute anyway.

How long did the build take, and what's the stack?

Seven weeks, shipping to iOS and Android from a single React Native codebase.

A Node.js service holds the WebSocket connections and fans position updates out to the riders subscribed to each ride. Google Maps supplies the map through react-native-maps, the Google Directions API supplies routes and arrival estimates, and the platform geolocation APIs supply the driver's position.

None of that is exotic. Every piece is a documented API used roughly the way its documentation suggests. What made the difference was choosing the right transport for the data first, and then refusing to re-render or re-request anything that hadn't changed.

Does this build cover driver dispatch or demand heat maps?

No. This case study covers the rider-facing side: live driver tracking, route rendering, arrival estimates, and the render and battery work that makes those usable in practice.

Dispatch assignment wasn't part of this build, and demand heat maps aren't something we build.

Ready to build something that matters?

We solve problems that don't have Stack Overflow answers. Let's talk.

Book a Discovery Call