We cut the billable Google Directions calls on a simulated 25-minute Pakka delivery from about 151 to 1. Both apps refetched the road line every ten seconds while the driver moved, and the server's cache key changed every 1.1 metres, so every call missed the cache. The fix fetches one route per leg, refetches only when the driver is more than 150 m off the drawn line (measured to the nearest segment, at most once every 30 seconds) and counts the ETA down on the phone.
This is the story behind one line in the Pakka Logistics case study: why the bug cost what it did, why a cache could not save us, and the test that stops the fix from quietly breaking the feature.
What was live tracking actually costing?
Live tracking in Pakka has two parts that look like one. The driver's position streams over a Socket.IO connection we already run, so moving the dot on the map costs nothing per update. The road line from the driver to the next stop comes from Google's Directions API through our backend, and every call that reaches Google is billed. Our cost model prices one call at ₹0.43: $0.005 at ₹85 to the dollar, from Google's public rate card (see Google Maps Platform pricing).
Four settings in the code decided the bill:
| Setting | Value |
|---|---|
| The route hook's effect key | coordinates rounded with toFixed(4), about 11 m |
| The route hook's refetch throttle | 10 seconds |
| The driver app's position updates | after 20 m of movement, at most every 10 seconds |
| The server's Directions cache key | coordinates rounded with toFixed(5), about 1.1 m |
A moving vehicle covers 11 m well inside ten seconds, so the effect re-ran on every throttle window. By then its origin had moved far more than 1.1 m, so the Redis lookup missed. That is six billable calls a minute for every open map: ₹2.58 a minute.
The customer's map was bounded by how long people watched it. The driver's was not. The driver's trip screen is the navigation view and stays open for the whole delivery by design, so a 30-minute trip made roughly 180 calls, about ₹77, on the driver's phone alone, whether or not anyone was watching.
Why didn't the Redis cache catch it?
The cache was built for a different job. getDirections keys on origin and destination rounded to five decimal places and keeps each result for ten minutes. That is right for a quote: the same pickup and drop asked twice within minutes gets one billed call, and the route preview, the fare quote and the driver's first route share it.
Tracking breaks that assumption. The origin is the driver, and a driver never stands still at 1.1 m precision. No expiry time helps a key that changes on every request. Coarser rounding would not have fixed it either: at any grid size a moving vehicle keeps crossing cell boundaries, and a coarse grid hands the driver a route that starts somewhere they are not.
The real problem sat upstream. The driver's coordinates were in the effect's dependency array, so movement itself was treated as a reason to ask Google again. The question we should have asked was: when is the drawn route actually wrong?
Our own cost alarms would not have caught it either. The backend counts Directions calls made while Redis is unavailable and warns once a run crosses a threshold. Here Redis was healthy. Every miss was a legitimate miss.
How did a bug like this survive?
Both call sites, the customer's active-booking screen and the driver's trip screen, carried the comment "One fetch per leg". The hook's own docblock, three files away, correctly described one fetch every ten seconds. Reading either call site told you the cost was fine, and both were written by someone who believed it. The comment stood for about two months.
The cost study repeated the mistake. Our per-trip unit-economics report recorded live tracking as ₹0.00, on the assumption that it rode the socket. The socket was there; the route was being refetched alongside it. We found the leak on 28 August 2026 while working out what a fleet-tracking subscription would have to cost, because at any plausible watch time the per-minute figure dwarfed the whole per-trip cost the report modelled. It was fixed the same day.
What does the fix change?
Nothing new needed building. Something needed to stop happening. The policy is now four rules:
- A leg change (pickup to drop) fetches a route.
- If nothing is drawn yet, fetch.
- If the driver is more than 150 m off the drawn line, fetch, but at most once every 30 seconds.
- Movement alone never fetches.
The 30-second floor matters at the boundary. A driver on a parallel service road, or a GPS reflection off a building, can sit just past 150 m for minutes. Without the floor that would refetch on every position update and bring the cost straight back.
Two client-side changes make the last rule possible. The old code refetched mostly so the ETA would keep counting down. Now the phone derives the leg's average speed once, from the distance and duration the route came back with, and applies it to the distance left along the line. And the line is trimmed to the road ahead, which costs nothing and doubles as a progress display.
The decision is one pure function in a file that is byte-identical in the customer and driver apps. The two hooks differ only in which API module they import.
export function shouldRefetchRoute({ driver, polyline, legChanged, lastFetchAt, now }) {
if (!driver) return false;
if (legChanged) return true; // a different destination
if (!polyline || polyline.length < 2) return true; // nothing drawn yet
const off = distanceToPolylineM(driver, polyline);
if (off <= DEVIATION_THRESHOLD_M) return false; // 150 m: on route, the common case
return now - lastFetchAt >= MIN_REFETCH_INTERVAL_MS; // 30 s floor at the boundary
}
Why measure to the nearest segment, not the nearest point?
This is the bug the fix could easily have introduced. Server polylines are sparse: on a straight run, two vertices can be a kilometre apart. A driver exactly halfway is on the road but about 500 m from both ends. A nearest-vertex check would call that a deviation and refetch, recreating the cost we were removing, and it would have looked perfectly reasonable in review.
distanceToPolylineM projects the driver onto each segment and clamps the projection to the segment's ends, which handles beside, past-the-end and before-the-start in one formula. It uses a local flat-earth projection rather than a great-circle cross-track formula: accurate over the tens of metres that matter here, and cheap enough to run on every socket tick. It stops scanning once the driver is within a metre of the line, because a route can have hundreds of points.
How did we prove it?
The claim is about a call count over a journey, so the test measures a count. A React hook cannot be asked that directly; a pure function can. The test builds a straight run of about 5 km with vertices roughly 1 km apart, as the server would return it, and replays a 25-minute drive with a position every 10 seconds. That is 151 positions, which is what the old policy would have billed. The test asserts exactly one fetch.
A test like that also passes if the code never fetches again. So the suite carries a negative control: the driver follows the route for five minutes, then turns hard east in steps of about 217 m, and the test asserts more than one fetch. A cost fix that silently leaves a wrong line on the map for the rest of the trip is worse than the cost.
Seven more cases pin the edges: the 30-second floor holds at the boundary, a leg change always refetches, an empty map always fetches, a driver midway between distant vertices measures under 5 m, the beside, past-the-end and before-the-start distances are right, the ETA falls as the driver advances with no extra fetch, and the remaining distance only ever shrinks. All nine pass in both apps. One unrelated test file already failed in both apps; we confirmed it failed before the change by stashing and re-running, rather than assuming.
What does one delivery cost to run now?
With tracking fixed, we priced one cash-paid booking line by line from the code. Each live-tracking line is now one Directions call per leg, two legs, ₹0.86.
| Step | Consumer trip | Business trip |
|---|---|---|
| Pickup and drop address search | ₹2.90 | ₹0.00 |
| Route preview | ₹0.43 | ₹0.00 |
| Map loads | ₹2.40 | ₹1.80 |
| Live-tracking routes, driver | ₹0.86 | ₹0.86 |
| Live-tracking routes, customer | ₹0.86 | ₹0.00 |
| Pickup and delivery SMS codes | ₹0.40 | ₹0.40 |
| Proof-of-delivery photo | ₹0.01 | ₹0.01 |
| Driver matching | ₹0.00 | ₹0.00 |
| Total | ₹7.86 | ₹3.07 |
Matching costs nothing because it runs on a MongoDB $geoNear query and local distance maths, never on Google. A business trip costs less than half because nobody types an address and the business portal draws no map; its tracking board is a status board built from our own database. What is left, ₹3.07, is the cost of executing the delivery itself.
The corrected table moved the consumer figure from ₹6.14 to ₹7.86. We left the old rows visible in the report under a correction banner instead of rewriting them, because ₹6.14 had already been quoted elsewhere and readers needed to see that it moved, and why.
These figures date from 28 August 2026, and one business line has changed since. Business quotes had been priced on straight-line distance × 1.25, which cost nothing; measured against 90 Bengaluru routes, that factor under-charged on 72 of them. Since 22 September they price on real road distance from Directions, cached per route, because a 43-paise route call recovers ₹23–67 of fare. A zero in a cost table is only good news if it is true.
What we'd do again, and what we'd do differently
Again:
- Put the cost decision in a pure function and assert a call count over a simulated journey, not a behaviour that looks right.
- Write the negative control in the same commit as the positive test.
- Log every billable call. The backend writes a line with its approximate cost in paise for each call that reaches Google, and warns on the two silent multipliers: the Directions cache not serving, and Places calls arriving without a session token.
Differently:
- Treat a comment that states a cost invariant as a claim to test. "One fetch per leg" should have had a line of code enforcing it from day one.
- Count billable calls per booking. The alarms we had watch the cache, not the caller, and a healthy cache serving a broken caller looks normal.
- Check the unit prices against a real bill. Ours are public rates converted at ₹85 to the dollar, and Google changed its Maps Platform pricing in 2025, so the report marks them to be verified against one real month of billing. The ranking of where the money goes does not depend on them; the rupee totals do.
- Measure watch time. We measured the per-minute rate, not how long customers actually keep the map open, so the total saved across real trips is still an estimate.
- Back off on failure, not only on success. "An empty map always fetches" is right when a route is simply missing, but when Google returns no route at all (an outage, a quota cut or a missing key) the map stays empty and every position update fetches again, and the booking screen's route preview retried in a tight loop until the rate limiter answered 429. We found it in October 2026 while capturing the apps against a local backend with no Google key. A failed fetch needs the same floor a successful one has.
If you are building a delivery or field-service app with live maps, our logistics app development page covers how we approach the rest of the stack.
