What the engine optimizes
Optimization is driven by an ordered list of objectives. The engine compares candidate solutions one objective at a time, in that order: the first objective that separates two solutions decides which one wins, whatever happens on the later ones. Order reflects business priority, not a weighted average. The default sequence covers most cases (99% according to Kardinal):The quality vs. computation time trade-off
There is no single “optimize until done” call, and no synchronous alternative that blocks until a solution is ready — the API is asynchronous only: submitting a plan returns as soon as it’s accepted, and you retrieve or poll the solution separately. Instead, you control how much time the engine is allowed to spend viamaxOptimizationDuration (an ISO 8601 duration, for example "PT10M"):
- The engine is guaranteed to never regress: each new solution it publishes is at least as good as the previous one on the preceding objective sequence. There’s no risk of “rolling back” to something worse.
- The longer you let it run, the better the solution can get — but returns diminish. If the engine hasn’t found an improvement in a while, it considers itself done, even before
maxOptimizationDurationelapses. maxOptimizationDurationonly counts time actually spent searching. It excludes queueing time (if no worker is free), and the time spent building the underlying math problem (fetching travel times, etc.). Add a margin before you fetch a solution to account for this.
Static — one shot, generous duration
Static — one shot, generous duration
Set a long
maxOptimizationDuration (for example, PT30M), submit the plan, and fetch the solution after that window (plus margin). Simple, but you wait for the full window even if the engine converged early.Iterative — short duration, manual restarts
Iterative — short duration, manual restarts
Use a short
maxOptimizationDuration and, if the solution isn’t good enough, restart optimization with PUT /plans/{planId}/running (body true). Updating the plan with changed data has the same restarting effect, but resubmitting an identical plan doesn’t: if nothing in the plan’s JSON has changed, optimization isn’t relaunched, its version stays the same, and it isn’t billed, so use the running endpoint to restart an unchanged plan. Restarting won’t help if the duration is too short for the problem size, or if the engine already considers the current solution final.Polling — preferred
Polling — preferred
Poll
GET /plans/{planId}/state — no more than once every 10 seconds per plan, less often on long optimizations — and read the solution and its objective values as it improves, deciding for yourself when the result is “good enough” for your business, without waiting for the engine to fully settle. This is the most responsive pattern and the one Kardinal recommends; see Polling the state and the solution for what to expect along the way.status field tracks a plan through its lifecycle — waiting room (if you’re over your max simultaneous running plans quota), creation (fetching travel times, building the problem), optimization, and, if predictive traffic is enabled, an asynchronous traffic-fetching stage running in parallel:
state summarizes that same lifecycle as a single value (waiting, processing, preOptimizing, preOptimized, optimizing, optimized, stopped, deleted, interrupted) instead of a breakdown per stage — optimized is the value to poll for once you only care about “is this done,” rather than inspecting every field inside status. Retrieve it directly with GET /plans/{planId}/state:
Response (abridged)
state tells you the plan has settled — it doesn’t carry the solution itself. Once state is optimized, fetch the actual result with GET /plans/{planId}/solution.Polling the state and the solution
- Poll
/stateno more than once every 10 seconds per plan, and space calls out further on long optimizations. optimizedisn’t the only end state. A plan stopped on request goes tostopped, then tointerrupteda few seconds later once the engine has actually stopped — the solution can still improve between the two. A version is alsointerruptedwhen a newer version of the plan is submitted. Always set a maximum wait on the client side, in case the state stops progressing.- Before the first solution exists (
waiting,processing),GET /plans/{planId}/solutionreturns404 NOT_FOUNDwithproperties.details: "Solution version not found."— not an error to give up on, just a solution that isn’t there yet. - The first solution published is often empty: every stop in
unaffectedStopIdsand no tours, published as soon as optimization starts, with the real solution following shortly after. A200on/solutiondoesn’t by itself mean the solution is usable. - After a
PUT,/solutionkeeps returning the previous version’s solution until the new version has one of its own — and indefinitely if the new version is stopped before being optimized. Compare the solution’splanVersionwith the version you expect;/statereturnsplanVersiontoo, telling you which version the state refers to.
Continuous and interactive optimization
Submitting a plan again with the sameid doesn’t start a new problem from scratch — it tells the engine “this is the same problem, with updated data.” The request still carries the complete plan, not only what changed (see Real-time re-optimization). If the plan’s content changed, the version number increments and the engine repairs the previous solution, adjusting it just enough to be valid for the new data, then keeps improving from there; an identical plan changes nothing. This first optimization on a given plan layout is slower (the engine is learning the problem’s shape); subsequent updates are typically much faster.
Because of this, if you update a plan while an older version is still optimizing, the engine does not let two versions be considered “optimized” at the same time — it stops work on the stale version in favor of the newest one.
What you control vs. what the engine controls
You control: the objectives list and its order,maxOptimizationDuration, when to restart or stop optimization (running: false), resource/order priority, and lateDeparture (whether resources leave as early as possible or depart later to reduce idle waiting time).
The engine controls: the actual search strategy, and the guarantee that solution quality never regresses between versions. Problem complexity — and therefore how much maxOptimizationDuration you should budget — is mostly driven by the number of stops and resources, and whether advanced constraints (alternative stops, LIFO removal strategy, overlapping-capacity limits) are in play; some of these switch the engine to alternative algorithms that are markedly slower. Traffic-aware vehicle profiles (withTraffic: true) don’t change the search itself: they only lengthen the fetching of travel-time matrices, which isn’t counted in maxOptimizationDuration. Allow for that extra time in how long you wait before fetching a solution, not in maxOptimizationDuration.
See Hard vs soft constraints for how the engine decides what’s non-negotiable versus what it can compromise on while searching for these objectives.
See also
- Modeling costs — configuring
costsominimizeCostshas something to optimize. - Real-time re-optimization — updating a plan already being optimized.
- Handling large volumes — sizing
maxOptimizationDurationfor production-scale plans.

