Keeping a route-optimization dashboard responsive with Celery + VROOM
Solving the Vehicle Routing Problem (VRP) — assigning stops to vehicles and ordering them under capacity and time-window constraints — is NP-hard. Even with a great solver like VROOM, a real instance can take seconds. That's fine for a batch job and fatal for a dashboard where someone just clicked Optimize.
The fix is old but reliable: get the work off the request path.
The shape of the problem
A synchronous endpoint that solves inline holds the connection open, ties up a worker, and times out under load. Instead, the API should accept the job, hand back an id, and let the client poll.
# api.py — FastAPI accepts the job and returns immediately
@app.post("/optimize")
async def optimize(req: OptimizeRequest):
task = solve_routes.delay(req.model_dump())
return {"job_id": task.id, "status": "queued"}
@app.get("/optimize/{job_id}")
async def status(job_id: str):
res = AsyncResult(job_id, app=celery_app)
return {"status": res.status, "result": res.result if res.ready() else None}
Celery does the heavy lifting
The actual solve runs in a Celery worker backed by Redis. VROOM gets the matrix from OSRM and returns ordered routes.
# tasks.py
@celery_app.task(bind=True)
def solve_routes(self, payload):
problem = build_vroom_problem(payload) # vehicles, jobs, capacities
solution = vroom_solve(problem) # calls VROOM + OSRM
persist_metrics(solution) # PostgreSQL
return serialize(solution)
Because the queue is concurrent, ten users can optimize at once without one blocking another — you just scale workers.
Polling, the client side
On the Next.js dashboard, TanStack Query turns polling into three lines. refetchInterval stops once the job is done.
const { data } = useQuery({
queryKey: ["opt", jobId],
queryFn: () => fetch(`/optimize/${jobId}`).then((r) => r.json()),
refetchInterval: (q) => (q.state.data?.status === "SUCCESS" ? false : 1500),
enabled: !!jobId,
});
What I'd tell past me
- Treat the solver as a black box behind a queue. It makes scaling a config change, not a rewrite.
- Persist every run. Optimization logs and metrics in PostgreSQL turned "is it working?" into a chart.
- Cache the OSRM matrix. Distance/duration matrices are the slow part; Redis-caching them per region cut solve setup time noticeably.
The whole thing shipped as Velora and picked up a Bronze at Kriti — but the real win was a dashboard that stays buttery while a solver grinds in the background.
Comments (0)
- Be the first to comment.