V3 is in beta. General availability (GA) is planned for January 2027.
POST /v3/routing/solve come down to three things you control: how long you let the search run, how large the problem is, and whether the search starts from scratch or from a plan you already have. This page covers each, plus how to read the result and what to do when the service is busy.
Where response time goes
A solve does two things in sequence:- Travel lookup. The API fetches road travel times and distances for the locations in the request. This grows with the number of distinct locations. See Distance matrices.
- Search. The solver builds an initial plan and then improves it for up to
options.time_limit_s, returning the best plan it found.
time_limit_s bounds the search only. The travel lookup, and any time the request waits for a free solver, come on top, so a search that uses its whole budget returns later than the budget you set.
Client timeout
Derive your HTTP client timeout fromtime_limit_s; do not hard-code one value for every request:
- Measure the overhead.
summary.timings.total_msis the time for the whole request. On representative requests sent without aplan,timings.total_msminus the search budget is what the travel lookup and the wait for a free solver cost you. The lookup grows with the number of distinct locations, so measure on your largest requests. - Set the timeout to
time_limit_splus that overhead, with margin. A timeout equal to the budget aborts requests that were about to succeed. - Raise the two together.
time_limit_scan be as high as300. A client timeout sized for a 5-second budget cuts off a 120-second one.
The time budget
number
default:"1"
Search budget in seconds; fractions are allowed (
2.5). Maximum 300; a larger value is rejected with 400 (error item code limit_exceeded, pointer /options/time_limit_s). A value below 0.1 is raised to a 100 ms floor.integer
default:"42"
Random seed for the search. Omitting
seed is the same as sending 42: it is not randomized per request, so re-sending a request without a seed does not explore a different plan. Repeating a request with the same seed usually returns the same plan, but the search is bounded by time and not by a fixed amount of work, so it is not guaranteed. To explore other plans, send different values.- Measure on your own data. Solve a representative request at a few budgets and compare
summary.score. Stop raising the budget when the score stops moving. - Past that point, vary the seed instead. Different seeds explore different plans, so running the same request with a few
seedvalues and keeping the best result is another way to spend spare time.
Problem size
The larger the problem, the more work each improvement step takes, so the same budget covers less ground. There is no budget that is right for a given number of jobs: start from the 1-second default, raisetime_limit_s in steps on a representative request, and keep the smallest budget after which summary.score stops improving.
What the request uses matters as much as how big it is. Driver breaks, time windows, and multi-trip reloads all make each step more expensive, so a request that uses them needs more time than a plain capacity-only request with the same number of jobs.
A request can hold up to 10,000 tasks (jobs plus shipments) and 2,000 vehicles. See Limits for every cap and the error each one returns.
Re-optimizing from an existing plan
When a plan already exists and something changed (a new job, a cancelled one, a vehicle running late), send the previous response’splan back as the top-level plan of the next request. This is a warm start: the search begins from that plan instead of building one from scratch, so the budget goes into improving a plan that is already good. The plan is a starting point, not a constraint: stops move wherever the search finds a better place for them, except the ones you freeze with plan.routes[].locked_count. See the V3 API model for the plan format.
A warm start can return before the budget
time_limit_s is an upper bound on a warm-started search, not a fixed duration. When the request carries a plan, the search stops as soon as it has gone 15% of the budget, and at least 300 ms, without finding a better plan. With time_limit_s: 10 that is 1.5 seconds without an improvement. The response is the best plan found up to that point, and summary.timings.total_ms can be well below the budget.
The early stop never applies to two kinds of request, which are bounded by time_limit_s alone:
- a request without a
plan - a request in which any shift lists
reloads, with or without aplan
timings.total_ms on a warm start is not a failure: the search found nothing better to do with the remaining time.
Options that add latency
options.polylinesadds a routing lookup after the search to trace each route’s road geometry. Leave it out unless you draw the routes.problem.travel.departure_timeandproblem.travel.engine: "osm"always fetch travel data fresh. Without them, a request with at most 200 distinct locations can reuse travel data looked up within the last hour. See Distance matrices.
Reading the result
Every solve response reports what the search did insummary:
integer
Wall-clock time for the whole request in milliseconds, including the travel lookup and the wait for a free solver. Compare it with
time_limit_s to see how much of your response time is search and how much is everything else. On a warm start it can be below time_limit_s.integer
Time spent in the search itself, in milliseconds. The rest of
total_ms is the travel lookup and the wait for a free solver.object
served, unserved and unserved_required: a job is one task and a shipment is one task. See Solution quality.Will more time place an unassigned job?
Checkunassigned[].category before raising the budget. It answers the question directly:
search_limit: yes. A feasible, worthwhile placement exists and the search did not find it in time. Raisetime_limit_sor try other seeds.blocked: unlikely. The job does not fit alongside the jobs that are planned. Add capacity or working time, or widen a time window.impossible,priced_out: no. The request has to change, or the job was left out because of thedrop_feeyou set.
When the service is busy
The API runs a bounded number of solves at once. A request that arrives when all solvers are busy waits briefly for a free one; if too many are already waiting, it is rejected immediately rather than queued indefinitely.
A
503 is a capacity signal, not a problem with your request, and the same request will succeed once a solver is free. If you send bursts (for example, re-solving every route at the top of the hour), spread them out or cap your own concurrency so the burst does not queue behind itself. See Errors for the full error body.
Checklist
- Start with the 1-second default and raise
time_limit_sonly when your own measurements show the score still improving. - Derive your client timeout from
time_limit_splus your measured overhead, and raise the two together. - Re-optimize with
planinstead of solving from scratch, and expect that a warm start can return before the budget is spent. - Read
unassigned[].categorybefore spending more time on unassigned jobs. - Retry
503afterRetry-After, and smooth out request bursts.
Related topics
Limits
Request size caps and the time-limit maximum
Distance matrices
What the travel lookup does and which fields control it
Solution quality
Reading the score and the route figures in a response
Errors
Status codes, error codes, and which failures to retry