A fast-growing London logistics startup was handling around 1,000 deliveries a week. Every route was built manually — the operations team spent hours each day crafting driver schedules by hand, juggling delivery windows, vehicle capacity, and driver availability in spreadsheets.

It worked at small scale. But as demand grew, the number of possible delivery combinations exploded. Each new driver or delivery didn't add complexity linearly — it multiplied it. The ops team became the bottleneck. Hiring more people wouldn't solve the problem; it would just move the ceiling slightly higher.

They needed a system that could handle complexity no human or spreadsheet could manage — and do it fast enough to route in real time.

100% of routes required manual human input

The cost matrix problem

At the heart of every vehicle routing system is a cost matrix — a table that tells the algorithm how expensive it is to get from any point to any other point. Every route optimisation algorithm works by minimising this matrix. For us, cost meant drive time.

The matrix has dimensions of n×n, where n is every collection, delivery, and driver start/end point. Populating it means calculating the drive time between every pair of locations — and we need to do this fresh for real-time routing.

The first problem was money

Standard mapping APIs charge per request. At the scale we were operating, populating the cost matrix every time was financially unviable. We replaced paid APIs with OpenStreetMap and a self-hosted GraphHopper server — calculating optimal drive times between any two points in near real-time, at zero marginal cost.

The second problem was time

Free doesn't help if it's slow. Populating every element of a large matrix sequentially still took around 50 minutes — far too long for a system that needed to produce routes on demand. We stacked three changes together to fix this:

Parallel requests. We adapted the GraphHopper server settings and provisioned enough compute to handle concurrent route calculations, rather than processing them one at a time.

Route caching. We stored computed routes so they're never calculated twice. But on its own, caching was ineffective — the exact same journey to the exact same latitude/longitude coordinate rarely repeats.

Hexagonal grouping. This is what made the caching work. We replaced precise coordinates with Uber's H3 hexagons, grouping nearby locations into a single identifier. Now we're not caching individual addresses — we're caching neighbourhoods. It sacrifices some accuracy, but the improvement in speed and cache hit rate is more than worth it.

One more optimisation: we assumed that the drive time from A to B is roughly the same as B to A. Not perfectly true on one-way streets, but practically negligible. This meant we only needed to populate half the matrix and mirror it.

~50 min → seconds Cost matrix computation time

Solving for scale

With the cost matrix fast and cheap, the next ceiling was the optimisation algorithm itself. Early versions worked fine at 1,000 deliveries. At 5,000 the system slowed. Beyond that, it wouldn't finish — the number of possible route combinations grows exponentially with each new delivery point.

Job grouping. Multiple deliveries heading to the same area were combined into single jobs. A van collecting three parcels from the same neighbourhood doesn't need three separate entries in the system. This cut the number of data points by 60%.

Divide and conquer. Rather than solving one massive routing problem for the whole city, we split it into geographic zones and solved each independently. One impossible problem became many manageable ones.

10x delivery volume handled with no degradation in solve time

Making it work in the real world

A textbook routing algorithm assumes every driver can go everywhere, at any time, carrying anything. Reality is messier.

Drivers have shift windows. Vehicles have weight and volume limits. Some jobs need specialist qualifications. Certain areas have predictable delays at certain times of day. We used constraint programming — a technique for solving problems with many interdependent rules, used widely in airline scheduling and shipping logistics. Every real-world constraint was encoded as a rule the system must satisfy, and it reasons about all of them simultaneously rather than applying them as afterthoughts that break the optimised routes.

This is what took routing from a theoretical exercise to something the operations team could actually trust — and stop touching.

100% → ~10% Manual intervention on routes

The results

Weekly deliveries ~1,000 ~10,000
Route planning Manual, hours/day Automated, minutes
Cost matrix computation ~50 minutes Seconds
Manual intervention 100% of routes ~10%
Ops team growth Zero additional hires

The routing system became infrastructure the business could grow on top of, rather than a process it had to grow around.