← Back to blog

How Uber Handles Millions of Location Updates Every Second

·
#distributed-systems#consistent-hashing#geospatial#uber

I’ve spent enough time on backend systems to know the panic of watching a write path fall over under load. Usually it’s a launch, a marketing push, something spiky and temporary. Uber’s location-tracking system doesn’t get that luxury. Every driver’s app sends a GPS ping every few seconds, all day, every day, in every city Uber operates in. During a normal rush hour, that’s a stream of updates climbing into the millions per second, sustained, not a spike.

The interesting part isn’t the number. It’s that no amount of “bigger database” fixes it. The actual fix was reframing the problem entirely, and that reframe is worth understanding even if you’ll never touch a system at this scale.

The Problem, Properly Framed

Here’s the naive design, the one anyone would sketch on a whiteboard in five minutes: one table, one row per driver, updated in place on every ping.

It works fine with a hundred drivers. At real scale it fails in two separate ways, and it’s worth being precise about both, because they get conflated a lot.

Failure one: it’s a write bottleneck. Millions of updates a second hitting the same storage layer, no matter how you shard by driver ID. Sharding spreads the writes, but it doesn’t change the fact that you’re still doing a point update per ping, and at this volume, point updates are expensive no matter where they land.

Failure two, and this is the one people miss: it’s slow for the query that actually matters. The real question the system needs to answer isn’t “did this write succeed.” It’s “which drivers are near this rider, right now.” A flat table with no spatial structure means answering that means scanning, or bolting on an index that becomes its own bottleneck the moment write volume climbs. You haven’t solved the problem. You’ve relocated it.

So the actual engineering question isn’t “how do we write faster.” It’s two narrower questions: who is responsible for tracking this one driver, and how do you find everyone near a point without checking everyone.

The Core Mechanics

Consistent Hashing: Deciding Who Owns This Driver

The first question, “which machine owns this driver’s current state,” sounds like it needs a lookup table. A lookup table becomes exactly the kind of central bottleneck you’re trying to avoid, and it’s a single point of failure to boot.

Uber’s open-source Ringpop library solves this with consistent hashing instead. Picture every server placed on a ring by hashing its identity. Each driver ID also hashes onto that same ring, and whichever server sits next going clockwise owns that driver. No central coordinator. Every server runs the same hash function and arrives at the same answer independently.

A minimal version of the idea looks like this:

import hashlib

class ConsistentHashRing:
    def __init__(self, replicas=100):
        self.replicas = replicas
        self.ring = {}
        self.sorted_keys = []

    def _hash(self, key):
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def add_node(self, node):
        for i in range(self.replicas):
            h = self._hash(f"{node}:{i}")
            self.ring[h] = node
            self.sorted_keys.append(h)
        self.sorted_keys.sort()

    def get_owner(self, key):
        h = self._hash(key)
        for ring_key in self.sorted_keys:
            if h <= ring_key:
                return self.ring[ring_key]
        return self.ring[self.sorted_keys[0]]  # wrap around

The replicas parameter matters more than it looks. Placing each node at just one point on the ring means adding or removing a node reshuffles a huge, unpredictable chunk of the keyspace. Placing it at a hundred points spreads each node’s ownership evenly around the ring, so adding a server only moves a small, proportional slice of drivers, not a arbitrary fraction of the whole fleet.

Gossip Protocols: Knowing Who’s Alive Without Watching Everyone

Consistent hashing tells you who should own a driver. It says nothing about whether that server is actually up. With thousands of nodes, having each one directly monitor every other one doesn’t scale, that’s an all-to-all communication problem layered on top of the one you were trying to solve.

Ringpop handles this with a gossip protocol, a variant of SWIM. Each server periodically pings a handful of random peers and exchanges what it currently believes about cluster membership. That information propagates through the whole cluster in a small number of hops, the same way a rumor spreads through a room faster than anyone would guess from how few people started it. No server needs a full picture of the cluster from direct observation, it just needs to talk to a few others regularly.

If a ping for a driver lands on a server that isn’t the current owner, that server just forwards it to whichever node the ring says is responsible. From the outside, callers see one coherent service. Underneath, it’s thousands of independent processes converging on the same picture of “who owns what,” with no single node in charge of the answer.

This is also where the design makes an explicit tradeoff worth naming directly: Ringpop is an AP system in CAP terms. If the network hiccups and two nodes briefly disagree about ownership, the system doesn’t halt and wait for consensus. It picks an answer and keeps serving. For a marketplace matching riders and drivers in real time, a rider getting matched to a driver based on slightly-stale ownership state beats a rider getting no match at all while two servers argue over who’s authoritative.

H3: A Spatial Index Built From Hexagons

Consistent hashing solves ownership. It does nothing for the second question: which drivers are near this point on the map.

The naive answer is comparing raw latitude/longitude against every driver, which is exactly the scan you’re trying to avoid. Uber’s answer is H3, an open-source geospatial index that divides the entire globe into hexagonal cells at multiple resolutions.

Hexagons specifically, not squares, and the reason is a real geometric property: every hexagon has exactly six neighbors, all the same distance from the center. A square grid has neighbors at two different distances, edge-adjacent versus diagonal, which quietly distorts any distance-based calculation you build on top of it. Hexagons don’t have that problem.

The grid is also hierarchical. Each hexagon subdivides into seven smaller hexagons at the next resolution level, so the same underlying system answers a city-wide query at a coarse resolution and a single-block query at a fine one, without switching data structures.

In practice, using it looks like this:

import h3

lat, lng = 37.7749, -122.4194
resolution = 9  # roughly city-block granularity

cell = h3.latlng_to_cell(lat, lng, resolution)

# the cell plus its immediate ring of neighbors
nearby_cells = h3.grid_disk(cell, 1)

Every driver ping gets converted to an H3 cell ID and filed under it. “Which drivers are nearby” stops being a scan and becomes a lookup against a cell and its immediate neighbors, a handful of cheap reads instead of a pass over the entire fleet.

The Tradeoffs / What Goes Wrong

None of this is free. Consistent hashing plus gossip means the system is, by design, eventually consistent about membership. During a real network partition, two servers can genuinely believe different things about who owns a driver for a short window, and a small number of requests get routed to the “wrong” owner before gossip catches up. That’s not a bug being tolerated, it’s the explicit price of staying available instead of blocking on consensus, but it does mean you can’t build logic downstream that assumes ownership is instantaneous and universally agreed on.

H3 has its own sharp edge: resolution choice is a real design decision, not a default you leave alone. Too coarse, and “nearby” pulls in drivers who are actually a fifteen-minute drive away because they share a large cell. Too fine, and you’re managing an enormous number of near-empty cells and paying more overhead per lookup than the win from precision justifies. Getting this wrong doesn’t crash anything, it just quietly produces bad matches, which is a hard class of bug to notice without a dedicated matching-quality metric watching for it.

And combining both systems means two independent sources of “slightly wrong,” ownership lag and cell-resolution imprecision, compounding on the same request. In production, that shows up as occasional low-quality matches that individually look like noise and only reveal a pattern when you aggregate across enough of them.

Takeaways

  • Splitting a scaling problem is often better than speeding it up. “Who owns this” and “what’s nearby” are genuinely different problems. Uber didn’t build one faster system, it built two narrow ones.
  • Consistent hashing removes a central bottleneck, but the replica count is the tuning knob that actually matters. Too few virtual nodes per server and rebalancing becomes lumpy and unpredictable.
  • Gossip protocols trade a little staleness for a lot of scalability. Nobody needs a complete, real-time view of a thousand-node cluster if peer-to-peer propagation gets everyone close enough, fast enough.
  • Choosing availability over consistency should be a documented decision, not an accident. Ringpop being AP is a deliberate fit for a system where “no answer” is worse than “a slightly stale answer.”
  • A spatial index’s resolution is a business decision disguised as a technical one. Too coarse or too fine both produce real, measurable damage, and the right value depends on what “nearby” actually needs to mean for your product.

If you found this useful or want to discuss it further, connect with me on GitHub or LinkedIn.