How Uber and Rapido Find Nearby Drivers in Real Time
How ride apps find nearby drivers in milliseconds: Geohash narrows the search, Redis stores live locations and Haversine filters by radius. Worked example included.
What you'll use
The problem: finding nearby drivers
Imagine you book a ride in Hyderabad. There are thousands of drivers online across the city. How does the backend find drivers near you within seconds, without checking every driver's location one by one?
Every driver continuously shares their GPS coordinates:
- Latitude
- Longitude
If the backend calculated the distance between the passenger and every online driver for every ride request, the computation would become expensive as the number of drivers grows. We need a way to narrow down the search before calculating distances.
That's where Geohash helps. In this article you'll see how Geohash, Redis and the Haversine formula work together.
How does Geohash work?
Geohash divides the Earth's surface into rectangular grid cells and represents each cell using a string of characters. For example, our passenger's pickup location is:
- Pickup
- 17.40044, 78.56073
- Pickup Geohash
- tepfgx
Suppose four drivers have the following locations:
| Driver | Latitude, Longitude | Geohash |
|---|---|---|
| Driver A | 17.41232, 78.54825 | tepg53 |
| Driver B | 17.40694, 78.59361 | tepgh2 |
| Driver C | 17.39912, 78.60995 | tepfuz |
| Driver D | 17.42066, 78.52963 | tepg4g |
Each driver belongs to a Geohash cell based on their coordinates. Instead of searching across the entire city, the backend can search the pickup cell and the relevant neighbouring cells. This significantly reduces the number of driver locations that need to be considered.
Where does Redis come in?
A real-time driver-tracking system needs fast access to frequently changing GPS locations. Redis can store each driver's latest location and associated Geohash. Whenever a driver sends a new GPS update, the backend updates their location in Redis.
When a passenger requests a ride, the backend:
- Reads the pickup coordinates.
- Identifies the pickup Geohash cell.
- Searches the cells that cover the requested search radius.
- Retrieves candidate drivers from those cells.
- Calculates the actual distance to each candidate.
- Filters out drivers outside the requested radius.
Redis ships with exactly that kind of index: the GEO commands. GEOADD stores each driver in a sorted set, using a Geohash-derived score, and GEOSEARCH (Redis 6.2+) returns the members inside a radius. Note that Redis takes longitude first.
# longitude first, then latitude
GEOADD drivers:online 78.54825 17.41232 driver:A
GEOADD drivers:online 78.59361 17.40694 driver:B
GEOADD drivers:online 78.60995 17.39912 driver:C
GEOADD drivers:online 78.52963 17.42066 driver:D
# drivers within 2 km of the pickup, nearest first
GEOSEARCH drivers:online FROMLONLAT 78.56073 17.40044 BYRADIUS 2 km ASC WITHDIST
# 1) "driver:A"
# 2) "1.8707"Redis does the radius filtering for you here. The rest of this article shows what that filtering actually means.
Why do we need neighbouring Geohash cells?
Our pickup Geohash is tepfgx, but Driver A's Geohash is tepg53. They are different strings, yet Driver A is close to the pickup location.
This is an important property of Geohash: two geographically close points can belong to different cells, especially when they lie on opposite sides of a cell boundary. Searching only the exact pickup cell can therefore miss nearby drivers. The backend must search the relevant neighbouring cells as well.
Also, sharing a Geohash prefix does not guarantee that two locations are close. A prefix can help narrow a search, but it cannot replace a proper radius search. Compare Driver A and Driver C against the pickup tepfgx:
| Driver | Geohash | Shared prefix | Actual distance |
|---|---|---|---|
| Driver A | tepg53 | tep (3 characters) | 1.87 km |
| Driver C | tepfuz | tepf (4 characters) | 5.22 km |
tepf identifies a four-character grid cell. Both the pickup location and Driver C fall inside it. tepg is a different four-character cell, and Driver A falls in that one. Think of Geohash cells as squares on a map: two points on opposite sides of a boundary can be very close even though their Geohashes differ, while two points inside the same larger square can be several kilometres apart.
tepfgx do not include Driver A's cell tepg53, even though Driver A is 1.87 km away. With 5 characters (cells of about 4.7 × 4.9 km), the pickup cell tepfg and its 8 neighbours (tepfd, tepfe, tepff, tepfs, tepfu, tepg4, tepg5, tepgh) cover all four drivers, and the distance filter then keeps only Driver A.So the correct approach in your application is:
- Encode the user's coordinates with
ngeohash. - Use Geohash prefixes and neighbouring cells to narrow the search.
- Calculate the actual distance to the candidate locations.
- Sort by distance and return the nearest one.
Don't sort Geohash strings alphabetically, and don't pick the one with the longest matching prefix. Geohash organises and filters locations. The actual distance calculation decides which one is nearest.
How do we calculate the actual distance?
Finding candidate drivers is only the first step. Suppose the passenger wants a driver within a 2 km radius. Let's calculate the distance to Driver C.
- Pickup coordinates
- 17.40044, 78.56073
- Driver C coordinates
- 17.39912, 78.60995
The coordinate differences are approximately:
- Latitude: 0.00132° ≈ 0.147 km
- Longitude: 0.04922° ≈ 5.22 km east-west at this latitude
The backend uses a geographical distance formula, such as the Haversine formula, to calculate the straight-line distance between the two coordinates. The calculated distance is approximately 5.22 km.
Deep diveSee the full Haversine calculation for Driver C, step by stepThe requested radius is 2 km, so Driver C is excluded. Now consider Driver A. The calculated distance between the pickup location and Driver A is approximately 1.87 km. Since 1.87 km is less than 2 km, Driver A passes the distance filter.
What happens to the drivers?
Here is the result after calculating the distances using the coordinates above, with a 2 km radius:
| Driver | Geohash | Approx. distance | Result |
|---|---|---|---|
| A | tepg53 | 1.87 km | Keep |
| B | tepgh2 | 3.56 km | Exclude |
| C | tepfuz | 5.22 km | Exclude |
| D | tepg4g | 3.99 km | Exclude |
The system keeps Driver A as a candidate because the driver is within the requested 2 km radius. The other drivers are excluded because they are farther away.
In a real implementation, drivers whose cells cannot intersect the search area can be eliminated before their exact distances are calculated.
The complete system design flow
Driver GPS location
Convert coordinates to Geohash
Update the latest location in Redis
Passenger requests a ride
Search the pickup cell and relevant neighbouring cells
Retrieve candidate drivers
Calculate the actual distance using Haversine
Apply the requested radius filter
Rank eligible drivers and match a driver
Key takeaway
Each component has a different responsibility:
| Component | Responsibility |
|---|---|
| Geohash | Narrows the geographical search area. |
| Redis | Provides fast access to frequently updated driver locations. |
| Haversine formula | Calculates the approximate straight-line distance between two GPS coordinates. |
| Radius filter | Removes candidates outside the requested distance. |
Together, these techniques make an efficient nearby-driver discovery system, and a useful building block for ride-hailing platforms, delivery applications and other real-time location-based systems.
In short
- 1
Drivers send GPS coordinates
Latitude + longitude
- 2
Convert to Geohash and update Redis
Store each driver's latest location
- 3
Passenger requests a ride
Pickup coordinates → tepfgx
- 4
Search the relevant Geohash cells
Retrieve nearby driver candidates
- 5
Calculate distance and filter
Keep suitable drivers within the radius
- 6
Match an available driver
Send the ride request
Haversine calculation, step by step
Here is the complete Haversine calculation for Driver C, using the exact pickup and driver coordinates.
1. Given coordinates
- Pickup latitude φ₁
- 17.40044
- Pickup longitude λ₁
- 78.56073
- Driver C latitude φ₂
- 17.39912
- Driver C longitude λ₂
- 78.60995
- Earth's mean radius R
- 6371 km
2. Calculate the coordinate differences
Δφ = φ₂ − φ₁ = 17.39912 − 17.40044 = −0.00132°
Δλ = λ₂ − λ₁ = 78.60995 − 78.56073 = 0.04922°3. Convert degrees into radians
The Haversine formula needs radians, not degrees. Radians = degrees × π / 180.
Δφ = −0.00132 × π/180 ≈ −0.0000230383
Δλ = 0.04922 × π/180 ≈ 0.00085905
φ₁ = 17.40044 × π/180 ≈ 0.303695
φ₂ = 17.39912 × π/180 ≈ 0.3036724. Apply the Haversine formula
a = sin²(Δφ / 2) + cos(φ₁) · cos(φ₂) · sin²(Δλ / 2)Calculate each part.
Part A (latitude component)
Δφ / 2 = −0.0000115192
sin²(−0.0000115192) ≈ 1.3269 × 10⁻¹⁰
Part B (longitude component)
Δλ / 2 = 0.00042953
sin²(0.00042953) ≈ 1.8449 × 10⁻⁷
Part C (cosines)
cos(0.303695) ≈ 0.954238
cos(0.303672) ≈ 0.954245
cos(φ₁) · cos(φ₂) ≈ 0.910577
0.910577 × 1.8449 × 10⁻⁷ ≈ 1.6799 × 10⁻⁷
Part D (add both components)
a ≈ 1.3269 × 10⁻¹⁰ + 1.6799 × 10⁻⁷
a ≈ 1.6813 × 10⁻⁷5. Calculate the distance
d = 2R · arcsin(√a)
√a = √(1.6813 × 10⁻⁷) ≈ 0.00041003
arcsin(0.00041003) ≈ 0.00041003 (for tiny angles, arcsin(x) ≈ x)
d = 2 × 6371 × 0.00041003
d ≈ 5.22 km6. Apply the 2 km radius filter
The actual distance is approximately 5.22 km and the requested radius is 2 km. Since 5.22 > 2, Driver C is excluded.
The same calculation in JavaScript
function haversineKm(lat1, lon1, lat2, lon2) {
const R = 6371; // Earth's mean radius in km
const toRad = (deg) => (deg * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(a));
}
// pickup -> Driver C
haversineKm(17.40044, 78.56073, 17.39912, 78.60995); // 5.2246 km (excluded)
// pickup -> Driver A
haversineKm(17.40044, 78.56073, 17.41232, 78.54825); // 1.8704 km (kept)Geohash encoding, bit by bit
Let's convert the pickup location into the Geohash tepfgx step by step, showing every range division, every binary bit and every character.
How Geohash turns coordinates into characters
Geohash uses a special Base32 alphabet. There are 32 characters, and the letters a, i, l and o are excluded. The characters used by our pickup Geohash are highlighted.
- Each character represents 5 binary bits.
- The algorithm generates bits by dividing geographic ranges in half, alternating between longitude and latitude.
- Longitude starts in the range [−180, 180]. Latitude starts in the range [−90, 90].
- If the coordinate is in the upper half, write
1. Otherwise write0. Then repeat with the smaller range. - Group every 5 bits and convert each group to one Base32 character.
A six-character Geohash therefore represents 30 bits: 15 for longitude and 15 for latitude.
The input
- Latitude
- 17.400441666657823
- Longitude
- 78.56072868878287
These are the full-precision coordinates. The rounded pickup 17.40044, 78.56073 used earlier produces the same Geohash.
Step A: divide the ranges
Initially, longitude runs from −180 to +180 and latitude from −90 to +90. The first longitude midpoint is (−180 + 180) / 2 = 0. The longitude 78.5607 is greater than 0, so the first bit is 1 and the algorithm keeps the upper half, 0 to 180.
Next it divides latitude at 0. The latitude 17.4004 is greater than 0, so the second bit is also 1. The algorithm keeps alternating longitude and latitude, halving the selected range each time.
Every one of the 30 divisions
- Calculate the midpoint: (minimum + maximum) / 2.
- If the coordinate is greater than or equal to the midpoint, output
1and keep the upper half. Otherwise output0and keep the lower half. - Alternate longitude, latitude, longitude, latitude until 30 bits are produced.
| Step | Axis | Range before | Midpoint | Comparison | Bit |
|---|---|---|---|---|---|
| 1 | Longitude | -180 to 180 | 0 | 78.5607 ≥ 0 | 1 |
| 2 | Latitude | -90 to 90 | 0 | 17.4004 ≥ 0 | 1 |
| 3 | Longitude | 0 to 180 | 90 | 78.5607 < 90 | 0 |
| 4 | Latitude | 0 to 90 | 45 | 17.4004 < 45 | 0 |
| 5 | Longitude | 0 to 90 | 45 | 78.5607 ≥ 45 | 1 |
| 6 | Latitude | 0 to 45 | 22.5 | 17.4004 < 22.5 | 0 |
| 7 | Longitude | 45 to 90 | 67.5 | 78.5607 ≥ 67.5 | 1 |
| 8 | Latitude | 0 to 22.5 | 11.25 | 17.4004 ≥ 11.25 | 1 |
| 9 | Longitude | 67.5 to 90 | 78.75 | 78.5607 < 78.75 | 0 |
| 10 | Latitude | 11.25 to 22.5 | 16.875 | 17.4004 ≥ 16.875 | 1 |
| 11 | Longitude | 67.5 to 78.75 | 73.125 | 78.5607 ≥ 73.125 | 1 |
| 12 | Latitude | 16.875 to 22.5 | 19.6875 | 17.4004 < 19.6875 | 0 |
| 13 | Longitude | 73.125 to 78.75 | 75.9375 | 78.5607 ≥ 75.9375 | 1 |
| 14 | Latitude | 16.875 to 19.6875 | 18.28125 | 17.4004 < 18.28125 | 0 |
| 15 | Longitude | 75.9375 to 78.75 | 77.34375 | 78.5607 ≥ 77.34375 | 1 |
| 16 | Latitude | 16.875 to 18.28125 | 17.578125 | 17.4004 < 17.578125 | 0 |
| 17 | Longitude | 77.34375 to 78.75 | 78.046875 | 78.5607 ≥ 78.046875 | 1 |
| 18 | Latitude | 16.875 to 17.578125 | 17.2265625 | 17.4004 ≥ 17.2265625 | 1 |
| 19 | Longitude | 78.046875 to 78.75 | 78.3984375 | 78.5607 ≥ 78.3984375 | 1 |
| 20 | Latitude | 17.2265625 to 17.578125 | 17.40234375 | 17.4004 < 17.40234375 | 0 |
| 21 | Longitude | 78.3984375 to 78.75 | 78.57421875 | 78.5607 < 78.57421875 | 0 |
| 22 | Latitude | 17.2265625 to 17.40234375 | 17.314453125 | 17.4004 ≥ 17.314453125 | 1 |
| 23 | Longitude | 78.3984375 to 78.57421875 | 78.486328125 | 78.5607 ≥ 78.486328125 | 1 |
| 24 | Latitude | 17.314453125 to 17.40234375 | 17.3583984375 | 17.4004 ≥ 17.3583984375 | 1 |
| 25 | Longitude | 78.486328125 to 78.57421875 | 78.5302734375 | 78.5607 ≥ 78.5302734375 | 1 |
| 26 | Latitude | 17.3583984375 to 17.40234375 | 17.38037109375 | 17.4004 ≥ 17.38037109375 | 1 |
| 27 | Longitude | 78.5302734375 to 78.57421875 | 78.55224609375 | 78.5607 ≥ 78.55224609375 | 1 |
| 28 | Latitude | 17.38037109375 to 17.40234375 | 17.391357421875 | 17.4004 ≥ 17.391357421875 | 1 |
| 29 | Longitude | 78.55224609375 to 78.57421875 | 78.563232421875 | 78.5607 < 78.563232421875 | 0 |
| 30 | Latitude | 17.391357421875 to 17.40234375 | 17.3968505859375 | 17.4004 ≥ 17.3968505859375 | 1 |
Step B: combine all 30 bits
Take the bit from every row, in order, without changing anything:
110010110110101011100111111101Split it into groups of five bits. Why five? Because 2⁵ = 32, so five bits can represent exactly 32 different values, from 0 to 31.
11001 01101 10101 01110 01111 11101Step C: convert each group to a character
In a five-bit group the place values are 16, 8, 4, 2 and 1. For example, 11001 = 1×16 + 1×8 + 0×4 + 0×2 + 1×1 = 25. In the Base32 alphabet above, index 25 is t. Do the same for all six groups:
| Group | Bits | Calculation | Decimal | Character |
|---|---|---|---|---|
| 1 | 11001 | 16 + 8 + 1 | 25 | t |
| 2 | 01101 | 8 + 4 + 1 | 13 | e |
| 3 | 10101 | 16 + 4 + 1 | 21 | p |
| 4 | 01110 | 8 + 4 + 2 | 14 | f |
| 5 | 01111 | 8 + 4 + 2 + 1 | 15 | g |
| 6 | 11101 | 16 + 8 + 4 + 1 | 29 | x |
Join the characters in the same order and the final Geohash is tepfgx: 30 bits, 6 groups, 6 characters.
Verify it with ngeohash
// npm i ngeohash
const ngeohash = require("ngeohash");
const latitude = 17.400441666657823;
const longitude = 78.56072868878287;
const geohash = ngeohash.encode(latitude, longitude, 6);
console.log(geohash); // tepfgx
console.log(ngeohash.decode(geohash));
// { latitude: 17.39959..., longitude: 78.55773..., error: {...} }
// the centre of the Geohash cell, not your exact point
console.log(ngeohash.neighbors("tepfg"));
// the 8 cells around a 5-character cell:
// [ 'tepg5', 'tepgh', 'tepfu', 'tepfs', 'tepfe', 'tepfd', 'tepff', 'tepg4' ]The package performs the same repeated range divisions and Base32 conversion internally.
Related blog
BookMyShow: HLD Breakdown
