Skip to main content
V3 is in beta. General availability (GA) is planned for January 2027.
This page describes the V3 API (/v3/routing). For the V2 API (/v2/vrp), see the V2 version of this page.
Response time and plan quality on 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:
  1. 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.
  2. 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, any time the request waits for a free solver, and the route geometry lookup when you set options.polyline, 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 from time_limit_s; do not hard-code one value for every request:
  • Measure the overhead. summary.timings.total_ms runs from the moment the request body is parsed to the end of the search. On representative requests sent without a plan, timings.total_ms minus 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. If you set options.polyline, measure the response time on your client as well: the geometry lookup comes after total_ms stops counting.
  • Set the timeout to time_limit_s plus that overhead, with margin. A timeout equal to the budget aborts requests that were about to succeed.
  • Raise the two together. time_limit_s can be as high as 60. A client timeout sized for a 5-second budget cuts off a 45-second one.
A response can also arrive before the budget is spent. See When a solve returns before the budget.

The time budget

number
default:"150 ms plus 4 ms per task"
Search budget in seconds; fractions are allowed (2.5). Omitted, it is 150 ms plus 4 ms per task (tasks are jobs and shipments together). Maximum 60; 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.
More time lets the search try more alternatives, with diminishing returns: most of the improvement comes early, and doubling the budget does not halve the cost. Two practical consequences:
  • 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 seed values and keeping the best result is another way to spend spare time.

See how fast the search converges

Every /v3/routing/solve response carries search_progress: the best plan’s score at each improvement during the search, so you can see how quickly the solver converges and choose a time_limit_s that is long enough and no longer. There is no option to request it. /v3/routing/evaluate does not search and leaves it out.
Entries come oldest first. The first is the plan the search started from (your plan, or the initial plan the solver built), each one between them is a strictly better best plan, and the last is the final plan: it equals summary.score, and its elapsed_ms equals summary.timings.search_ms; it can equal the entry before it, or be slightly worse than it. elapsed_ms is the search clock, starting near 0, so the travel lookup and the wait for a free solver are not on it. unassigned_priority and cost mean what they mean in summary.score, with cost in the objective’s currency. The best plan has the lowest unassigned_priority, then the lowest cost, so cost can rise when more tasks get served: in the example, every required task is served from 180 ms on, at a higher cost, and the rest of the search lowers it. A curve that goes flat long before search_ms means the rest of the budget bought nothing. The search is paced to time_limit_s, so read the curve as a guide and confirm a shorter budget by solving with it. At most 500 entries are returned: a longer curve is thinned evenly, keeping both ends. Some large solves without a plan return only the final entry.

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 default, raise time_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; 400 on POST /v3/routing/solve, larger problems go to async runs) 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’s plan 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.

When a solve returns before the budget

time_limit_s is an upper bound, not a fixed duration. Three kinds of solve can finish sooner: In each case the response is the best plan found, and summary.timings.search_ms and total_ms can be well below the budget. A short search here is not a failure: the search found nothing better to do with the remaining time. Every other solve runs for its whole time_limit_s. That includes any request in which a shift lists reloads, with or without a plan, and any request without a plan that has more than one vehicle or shift. The one exception is a busy service, which can shorten any solve’s search; see When the service is busy.

Options that add latency

  • options.polyline adds a routing lookup after the search to trace each route’s road geometry. It is not counted in summary.timings.total_ms. Leave it out unless you draw the routes.
  • problem.travel.departure_time, problem.travel.engine: "tomtom_real_time" and any request with more than 200 distinct locations always fetch travel data fresh. Without them (free-flow osm times, the default, or tomtom without a departure_time), 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 in summary:
integer
Wall-clock time in milliseconds from the moment the request body is parsed to the end of the search, including the travel lookup and the wait for a free solver. It does not include the options.polyline lookup or sending the response. Compare it with time_limit_s to see how much of your response time is search and how much is everything else. It can be below time_limit_s when the solve returns before the budget.
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, unassigned and unassigned_required: a job is one task and a shipment is one task. See Solution quality.

Will more time place an unassigned job?

Check unassigned[].category before raising the budget. It answers the question directly:
  • time_limit: yes. A feasible, worthwhile placement exists and the search did not find it in time. Raise time_limit_s or 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 the drop_cost you set.
Categories on the Errors page describes each one and the fix that goes with it.

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. While other requests are waiting, the solve that gets a solver searches for time_limit_s × solvers ÷ (solvers + waiting requests), never less than 100 ms: half its budget when as many requests wait as there are solvers. summary.timings.search_ms shows the search time it got. 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

  1. Start with the default budget and raise time_limit_s only when your own measurements show the score still improving.
  2. Derive your client timeout from time_limit_s plus your measured overhead, and raise the two together.
  3. Re-optimize with plan instead of solving from scratch, and expect that a warm start or a one-vehicle solve can return before the budget is spent.
  4. Read unassigned[].category before spending more time on unassigned jobs.
  5. Retry 503 after Retry-After, and smooth out request bursts.

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