Same Three Files, Much Harder Problem

When I finished porting the routing engine to Python, I had a 480-line file that solved vehicle routing problems and printed results to a terminal. That’s useful exactly to me, in exactly one context. The staff scheduler had already gone through the same transition — terminal script to Flask web app — and I’d figured out the pattern there. So I assumed wrapping the VRP would be roughly the same amount of work.

It wasn’t the same amount of work. But the structure was.

The flask VRP web app deployment on Render ended up using the same three-file skeleton I’d used for the scheduler: app.py handles the web layer, vrp_engine.py is pure Python with no Flask dependency, and templates/index.html is the frontend. Same pattern. Much harder problem inside.

The timeout problem hits differently at this scale

The staff scheduler solves in under a second. CBC finds optimal solutions fast for MILP problems at that size. I deployed it with Gunicorn’s default 30-second worker timeout and never thought about it again.

The VRP is different. With 113 stops and 10 vehicles, the OR-Tools solver runs for as long as you give it. The default search time limit in my parameters is 120 seconds — two full minutes. That’s intentional: longer search usually means better routes.

The first time I deployed the VRP to Render and ran a solve, I got a 502 error. The worker timed out at 30 seconds and died mid-solve, returning nothing. The fix is a one-line change to the Gunicorn start command:

gunicorn app:app --timeout 300 --workers 1

Five minutes instead of thirty seconds. The --workers 1 is there because the Starter plan has one shared CPU — multiple workers fight each other more than they help. I’ve since used this same start command for every OR model I deploy and never touched it again. But the first deployment failing because of a timeout I hadn’t thought about was exactly the kind of thing that takes an afternoon to debug when you don’t already know what you’re looking for.

The Excel header problem

The sample workbook has four sheets: locations, vehicles, parameters, and solution. The locations sheet has 113 rows of delivery data plus a header row with column names. Obvious to any human. The first version of the parser I wrote didn’t handle headers — it read row 0 as data, tried to interpret “lat” as a latitude coordinate, and threw a type error.

The fix I landed on was to auto-detect headers: if the first cell in the sheet is a non-numeric string, skip the row. This works cleanly for every sheet in the workbook and means users don’t need to know or care whether the engine expects headers. They just upload the file. I later applied the same pattern to the solution sheet — if it has a header row but no data rows, the engine treats it as an empty solution (fresh solve, no warm start). That matters because the warm-start feature reads prior routes from the solution sheet, and an empty sheet with only a header should not trigger it.

(The warm-start feature lets you re-run the solver starting from a previously found solution rather than from scratch. Useful when you want to refine routes without starting over. The StartFromLastSolution parameter controls it — default is false, and the engine also ignores it if the solution sheet is genuinely empty, regardless of what the parameter says.)

What the app actually does

You upload an Excel file. The app reads the locations sheet — each row is a delivery stop with lat/lon coordinates, demand, service time, and time windows. The vehicles sheet defines each truck’s capacity and costs. The parameters sheet controls solver behavior: search time, penalties for dropped orders, minimum drive time between stops.

The engine builds the OR-Tools routing model, fetches a real drive-time matrix from OSRM (a free open-source routing service), runs the solver, and returns routes. The output is a table showing each vehicle’s stops in order, arrival times, and whether any stops were dropped (undeliverable given the constraints). The whole solve takes between 30 seconds and 2 minutes depending on the dataset and search time limit.

There’s a second version of the frontend at /datos with a different color scheme — navy and orange instead of black and teal. I built it as an alternate theme and kept both live. They run the same engine; it’s purely cosmetic. (I genuinely don’t know which one I prefer. I’ve been switching between them for months.)

$14 a month and the distance matrix cache

The staff scheduler costs $7/month on Render’s Starter plan. The VRP is another $7/month. That’s $14 total for two live OR demos, publicly accessible, running real solvers. I find that number genuinely remarkable given what it would have cost to host something equivalent ten years ago.

The one operational complication is the distance matrix. The VRP needs real drive times between every pair of stops — for 113 stops that’s 113×113 = 12,769 distances. OSRM provides them free but the call takes a few seconds. Running it on every solve would be slow and wasteful. So I cache the matrix to disk as a NumPy array, keyed by an MD5 hash of the coordinates. Same stops, same matrix — load from cache. Different stops, compute fresh and cache the result.

On Render, the cache lives in the app’s container and gets cleared on every redeploy. That’s fine — the OSRM call is fast enough that recomputing occasionally isn’t a problem. The cache is mostly there to make repeated test runs during development bearable, not to handle production load (there isn’t much production load on a $7 Render service).

The pattern held

When I built the staff scheduler, I didn’t know if the three-file Flask pattern would generalize. One model is a sample. Two models with the same structure starts to feel like a system.

Same Flask skeleton. Same Render deployment. Same sample workbook with headers and a blank solution sheet. Same start command. The engine is completely different — PuLP MILP versus OR-Tools metaheuristic, 250 lines versus 480 lines, solves in a second versus solves in two minutes — but the wrapper around it is nearly identical. That’s the part I didn’t expect to work as cleanly as it did.

The app is live at vrp-scheduler.onrender.com. The sample workbook is the Ohio dataset — 113 stops, 10 vehicles, real coordinates from a logistics run. You can download it from the app and run it. The solver will find routes.

Tradeline Supply
Things that I use, like, and am affiliated with:
Mint Mobile offers great cell phone service for $15 flat, get $15 off using the link. Get discounted phones with service activation and no contract.
I never spend money before I check Mr Rebates or Rakuten to get cashbacks, rebates, discounts, coupons or cheaper gift cards.

Leave a Reply