Leveraging Redis for Fast Geospatial Queries
When you need to track dynamic points of interest—such as moving vehicles in a ride-sharing app or real-time delivery tracking—traditional disk-based databases like PostGIS may struggle with the high write throughput. This is where Redis GEO commands shine.
How Redis GEO Works
Under the hood, Redis GEO utilizes Sorted Sets (ZSET). When you add a geospatial point using GEOADD, Redis calculates a 52-bit Geohash for the latitude and longitude, and uses this Geohash as the score for the sorted set. This allows Redis to store and retrieve locations rapidly in memory.
Key Commands
GEOADD key longitude latitude member: Adds a location to the index.GEODIST key member1 member2 [unit]: Calculates the distance between two members.GEOSEARCH key [FROMLONLAT|FROMMEMBER] [BYRADIUS|BYBOX]: The modern way to query for members within a specific radius or bounding box.
Handling High Write Throughput
In a system like Uber, millions of drivers send location updates every few seconds. To handle this:
- Sharding: You cannot store all global locations in a single Redis node. Shard the Redis cluster geographically (e.g., one shard per city or country).
- Expiry: Redis key TTL applies to the whole GEO sorted set, not one member. Store per-driver freshness separately and remove stale members with a sweeper or atomic maintenance script; every query must reject candidates older than its freshness limit.
- Batching: Use Redis pipelines to batch
GEOADDcommands from the ingestion service to the Redis nodes.
Example of adding a driver and searching for nearby drivers:
GEOADD drivers -122.4194 37.7749 "driver123"
GEOSEARCH drivers FROMLONLAT -122.4194 37.7749 BYRADIUS 5 km WITHDIST
Worked example: one million active drivers
- Each driver reports every 4 seconds: 250,000 GEOADD per second globally.
- Shard by city: a big city with 50,000 active drivers produces 12,500 writes per second to its key, well within a single Redis primary when pipelined.
- Memory: a sorted-set entry with a short member name costs roughly 60 to 100 bytes including the skiplist and hash overhead, so 1 million drivers is on the order of 100 MB. Memory is not the bottleneck; write rate and hot keys are.
- Queries: a rider search is GEOSEARCH with a 3 km radius. Its cost is O(N + log M), where N is the number of members in the grid-aligned area scanned around the circle and M is the set size, so dense downtown queries cost more than suburban ones.
Freshness without per-member TTL
Keep two keys per city: drivers:geo:sf (GEO) and drivers:seen:sf (sorted set scored by last update time). On each update: GEOADD drivers:geo:sf -122.4194 37.7749 d123 and ZADD drivers:seen:sf 1727700000 d123. A sweeper every few seconds runs a Lua script that reads ZRANGEBYSCORE drivers:seen:sf -inf now_minus_30 and removes those members from both keys atomically. Queries still check last-seen for each result, because a sweep can lag.
Options for the hot location store
| Option | Write rate | Query | Weakness |
|---|---|---|---|
| Redis GEO per city | Very high | GEOSEARCH radius or box | Manual staleness; hot cities need sub-sharding |
| Redis hashes keyed by H3 or S2 cell | Very high | Read the k-ring of cells, filter | More client logic; easy to shard by cell |
| PostGIS with GiST | Moderate; each update rewrites an index entry | Rich SQL and polygons | Index churn and vacuum under constant updates |
| Custom in-memory service | Highest | Anything | You build replication and failover |
Operational notes
- Redis GEO accepts latitudes only between about -85.05 and 85.05 degrees (the Web Mercator limit).
- Replicas can serve reads to spread query load; accept that they lag slightly.
- After a failover, positions refill within one reporting interval, so treat Redis as rebuildable soft state and keep the durable trip record elsewhere.