Real-world system design
How Uber scales, from one server to a billion users
How a ride-hailing app grows from one city to the whole world — handling a flood of driver locations, matching riders in real time, and pricing on the fly. Based on Uber's public engineering writing, in AWS terms.
Play it — turn the traffic up yourselfStage 1 of 5
One city (~50K users)
- Mobile appMobile application
- EC2Amazon EC2
- RDSAmazon RDS
What breaks
Every driver's phone reports its location every few seconds, and one dispatch server is buried under the flood of updates on top of matching riders.
A ride-hailing app is write-heavy in a way a normal app is not: thousands of drivers each send a GPS ping every few seconds, all day. On one server, ingesting that firehose and scanning every driver to find the nearest one to a rider both happen on the same box — and the location flood alone outgrows it long before the city is large.
The fix
Location updates flow into a stream instead of the database, and a consumer keeps a live in-memory index of where every driver is — organised on a hexagonal grid (Uber's H3) so 'nearest drivers to this point' is a lookup, not a scan of everyone. The stream decouples the firehose from the matcher, and the database is left for trips.
GPS pings are a stream, not database rows — ingest them into an in-memory geo-index, off the request path.
Based on: H3: Uber's Hexagonal Hierarchical Spatial Index (Uber Engineering, 2018)
Stage 2 of 5
Many cities (~1M users)
- Mobile appMobile application
- Kinesis Data StreamsAmazon Kinesis Data Streams
- MemoryDBAmazon MemoryDB for Redis
- ALBApplication Load Balancer
- EC2 Auto ScalingEC2 Auto Scaling Group
- RDSAmazon RDS
What breaks
Locations are handled, but every ride request, fare estimate and trip lookup still reads the one trips database, and it is saturating.
With pings off on a stream, the matcher scales out. But riders opening the app, estimating fares and tracking trips all read from the same primary database, and reads pile up on it first. The one database becomes the bottleneck the dispatch fleet no longer is.
The fix
A cache holds the hot trip and fare data in memory, so most reads are answered without touching the database, and a read replica serves the rest. Reads scale sideways and the primary is left for trip writes.
With locations streamed, the read database is the next wall — a cache and a replica take it off the primary.
Based on: How Uber scales real-time dispatch and pricing (High Scalability, 2016)
Stage 3 of 5
Surge (~5M users)
- Mobile appMobile application
- Kinesis Data StreamsAmazon Kinesis Data Streams
- MemoryDBAmazon MemoryDB for Redis
- ALBApplication Load Balancer
- EC2 Auto ScalingEC2 Auto Scaling Group
- ElastiCache RedisAmazon ElastiCache for Redis
- RDSAmazon RDS
What breaks
The dispatch fleet is computing surge prices for every zone on every request, on top of matching — and that pricing work is saturating it.
Surge pricing compares supply and demand in every geofenced zone and reprices constantly. Doing that inside the dispatch service, on the path of every ride request, means the matcher is spending its time on pricing maths — and at five million users the fleet buckles under work that does not need to happen on the request.
The fix
Supply and demand events flow into a stream, and a separate pricing service recomputes each zone's surge on a fixed interval and publishes the number. Dispatch simply reads the current multiplier — the heavy pricing maths runs on its own service, on its own schedule, off the matching path.
Pricing is not part of matching — compute surge in its own service off a stream, and let dispatch just read the number.
Based on: How Uber scales real-time dispatch and pricing (High Scalability, 2016)
Stage 4 of 5
Global (~50M users)
- Mobile appMobile application
- Kinesis Data StreamsAmazon Kinesis Data Streams
- MemoryDBAmazon MemoryDB for Redis
- ALBApplication Load Balancer
- EC2 Auto ScalingEC2 Auto Scaling Group
- ElastiCache RedisAmazon ElastiCache for Redis
- RDSAmazon RDS
- MSKAmazon MSK
What breaks
One region is matching rides for the whole world — the dispatch fleet is stretched, and a rider on another continent waits on a round-trip across the ocean.
Matching is inherently local — a rider in Delhi is only ever matched with drivers in Delhi — so serving the planet from one region is both a capacity wall and a latency one, sending every request across the world and back. The market is geographic; the system should be too.
The fix
Because matching never crosses cities, the world shards cleanly by geography: each region runs its own dispatch, geo-index and trips, owning the cities in it, and routing sends every request to the region that owns that city. Latency drops and each region carries only its own market.
Ride-matching is local, so the world shards by geography — a full stack per region, each owning its cities.
Based on: How Uber scales real-time dispatch and pricing (High Scalability, 2016)
Stage 5 of 5
Planet scale (~150M users)
- Mobile appMobile application
- Route 53Amazon Route 53
- ALBApplication Load Balancer
- EC2 Auto ScalingEC2 Auto Scaling Group
- MemoryDBAmazon MemoryDB for Redis
- DynamoDBAmazon DynamoDB
At planetary scale Uber is many regional marketplaces behind geo-routing — each region owns its cities' drivers, riders and trips, and no request leaves the continent it belongs to.
Based on: How Uber scales real-time dispatch and pricing (High Scalability, 2016)
Now try it yourself
Reading is one thing. Turn the traffic up on Uber's architecture, watch it break, fix it — then build the whole thing from a blank canvas.
Play the scale journey