> ## Documentation Index
> Fetch the complete documentation index at: https://docs.solvice.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Handling infeasibility

> How to recognise a plan that does not cover every task, read the category, reason codes and typed violations that say why, and choose the fix

> [!NOTE]
>
> **V3 is in beta.** General availability (GA) is planned for January 2027.

A routing problem is infeasible when no plan can serve every task within the rules you sent, and partly infeasible when some tasks fit and some do not. On the V3 Routing API that is a normal outcome, not a failure. The solver returns the best plan it found and lists each task it left out with a **category** and one **reason code** per rule that refused it. `POST /v3/routing/evaluate` scores any plan you build by hand and reports each broken rule as a typed violation with how far over the limit it is. You never have to guess which rule, on which field, kept a task out.

Three steps, in order: recognise, diagnose, fix. Every response excerpt below is real output from a four-task, one-vehicle problem solved with `travel.engine: "straight_line"`.

## Recognise

### It is a `200`, not an error

A `4xx` means the request itself was wrong and nothing was planned; see [Errors](https://docs.solvice.io/guides/vrp/v3/errors.md). A plan that leaves tasks out is a `200`. Check `summary.tasks` on every solve response:

```json theme={null}
"tasks": { "served": 2, "unassigned": 2, "unassigned_required": 2 }
```

Then read `unassigned[]`, one entry per task on no route:

```json theme={null}
{
  "unassigned": [
    {
      "task": "job-fridge",
      "category": "impossible",
      "reasons": [
        { "constraint": "required_skills", "message": "A vehicle lacks a skill this task requires" }
      ],
      "relaxations": []
    },
    {
      "task": "job-heavy",
      "category": "impossible",
      "reasons": [
        { "constraint": "capacity", "message": "Capacity refused this task on at least one vehicle" }
      ],
      "relaxations": [
        {
          "pointer": "/problem/jobs/1/pickup_load",
          "op": "reduce_load",
          "amount": 15,
          "unit": "load",
          "message": "Reduce this task's load (its `pickup_load` or `delivery_load`) by 15, or add 15 capacity to an eligible vehicle."
        }
      ]
    }
  ]
}
```

The four fields to read:

| Field | What it tells you |
| - | - |
| `summary.tasks.unassigned` | Tasks on no route. `unassigned_required` is the subset without a `drop_cost`: must-serve tasks that were not served |
| `unassigned[].category` | What kind of fix can work: `impossible`, `blocked`, `priced_out` or `time_limit` |
| `unassigned[].reasons[].constraint` | Which rule refused the task, one entry per rule, in the vocabulary of evaluate's violation codes. Absent for `priced_out` and `time_limit`, where no rule did |
| `unassigned[].relaxations[]` | The smallest numeric change that would let it fit, with a JSON Pointer to the field: `extend_time_window` in seconds or `reduce_load` in load units. Empty when the fix is structural |

Pass `unassigned[]` through to the people who maintain the data: `message` is written for them, and `pointer` names the exact field in the request you sent.

### On evaluate: typed violations

`/v3/routing/evaluate` does not search, so a plan you send can break hard rules. Each broken rule is a `violations[]` entry on the stop or route that breaks it, and `summary.score.violation_count` counts them. The entry carries a stable `code`, and for the numeric rules an `excess` with its `unit`:

```json theme={null}
{
  "type": "job",
  "id": "job-early",
  "arrival": "2026-07-01T08:11:38+02:00",
  "departure": "2026-07-01T08:21:38+02:00",
  "lateness_s": 398,
  "violations": [
    { "code": "time_window", "message": "Arrives 398s after latest allowed time", "excess": 398, "unit": "s" }
  ]
}
```

Tasks the plan leaves out come back in the same `unassigned[]` shape as on solve, but `category` then says whether the task could be added to the plan as it stands: `time_limit` means it fits and is unassigned only because your plan omits it. See [Violations](https://docs.solvice.io/guides/vrp/v3/evaluate.md#violations) and [What unassigned means on evaluate](https://docs.solvice.io/guides/vrp/v3/evaluate.md#what-unassigned-means-on-evaluate).

### What the solver accepts without telling you

A task is unassigned either because a hard rule refused it or because serving it would cost more than its `drop_cost` (`category: "priced_out"`). Everything below is accepted and priced, or accepted and free, so a plan with `unassigned: []` and `violation_count: 0` can still hold outcomes you did not intend. Check the field in the right-hand column.

| What you sent | What the solver does | Where it shows |
| - | - | - |
| `time_windows[].per_late_hour` or `objective.per_late_hour` | Serves the task late and charges the hours. Not a violation, not unassigned | `stops[].lateness_s`, `score.components.lateness`. See [Time windows](https://docs.solvice.io/guides/vrp/v3/constraints.md#time-windows) |
| `drop_cost` on a task | Leaves the task out whenever serving it costs at least the fee | `category: "priced_out"`, `score.components.drop_costs`; not counted in `unassigned_required`. See [Priority and optional tasks](https://docs.solvice.io/guides/vrp/v3/constraints.md#priority-and-optional-tasks) |
| No `per_route` or `per_used_vehicle` (the default) | Opens another vehicle whenever that saves any cost at all, however small | `summary.vehicles_used`, `shifts_used`. See [Vehicle count is free until you price it](https://docs.solvice.io/guides/vrp/v3/concepts/scoring-explanation.md#vehicle-count-is-free-until-you-price-it) |
| `shifts[].max_overtime_s` without `per_overtime_hour` | Runs past `to` with no overtime premium, up to the grant; the hours still count in `per_route_hour` | `time.overtime_s` is non-zero while `score.components.overtime` stays `0.0`. See [Shift hours and overtime](https://docs.solvice.io/guides/vrp/v3/constraints.md#shift-hours-and-overtime) |
| `target` on a task | Misses the target and charges the hours; a target never makes a task unassignable | `stops[].target_lateness_s`, `score.components.target` |
| A window that opens after arrival | Waits, unpriced unless `per_wait_hour` is set | `stops[].wait_s`, `summary.time.wait_s`. See [Reading `summary.score.components`](https://docs.solvice.io/guides/vrp/v3/concepts/scoring-explanation#reading-summaryscorecomponents) |
| `preferences[]`, `preferred_tags[]` | Assigns against the preference and pays the `violation_cost` | `score.components.preferences`, `tag_preference`. See [Soft constraints](https://docs.solvice.io/guides/vrp/v3/concepts/constraint-system.md#soft-constraints) |
| Travel times the provider could not deliver | Plans on straight-line estimates instead of failing | `summary.travel_estimated: true`. See [Travel quality](https://docs.solvice.io/guides/vrp/v3/concepts/solution-quality.md#travel-quality) |

To make a priced rule hard again, remove the rate or the overtime grant. Removing `drop_cost` does not make a task unassigned: it makes the task required, so one that still cannot be served comes back as `impossible` or `blocked` instead of `priced_out`.

## Diagnose

**1. Group the unassigned tasks by category**

`category` decides which of the fixes below can work at all. `impossible` means no vehicle can ever serve the task: fix the request. `blocked` means a vehicle could serve it alone but not alongside the planned tasks: add capacity or working time, or relax a rule. `priced_out` means a slot exists and serving it costs at least its `drop_cost`. Only `time_limit` is a reason to give the search more time. In the response above both are `impossible`, so no amount of search time changes it. [Categories](https://docs.solvice.io/guides/vrp/v3/errors.md#categories) has the full definitions.

**2. Map each reason code to the field that caused it**

Read `reasons[].constraint` and go to the field it names. A task lists each rule once, so two entries mean two different rules refused it on different vehicles.

| `constraint` | Typical cause | Field to inspect | Guide |
| - | - | - | - |
| `unreachable` | Coordinates in `[latitude, longitude]` order, or a point far from any road | `jobs[].location`, `shifts[].start`, `shifts[].end` | [Reachability](https://docs.solvice.io/guides/vrp/v3/constraints.md#reachability) |
| `required_skills` | A skill name spelt differently on job and vehicle, or `min_level` above every vehicle's `level` | `jobs[].required_skills[].name`, `vehicles[].skills` | [Skills](https://docs.solvice.io/guides/vrp/v3/constraints.md#skills) |
| `serves_tags` | Every shift that serves tags lacks the job's tag | `jobs[].tags`, `shifts[].serves_tags` | [Tags](https://docs.solvice.io/guides/vrp/v3/constraints.md#tags-serves_tags-and-preferred_tags) |
| `eligible_vehicles` | `allowed` names no vehicle that can reach the task, or `excluded` covers them all | `eligible_vehicles.allowed`, `.excluded` | [Vehicle eligibility](https://docs.solvice.io/guides/vrp/v3/constraints.md#vehicle-eligibility) |
| `capacity` | A load above every vehicle's capacity (`impossible`), or full vehicles (`blocked`); a dimension named on the job but not on the vehicle | `pickup_load`, `delivery_load`, `vehicles[].capacity`, `shifts[].capacity` | [Capacity](https://docs.solvice.io/guides/vrp/v3/constraints.md#capacity) |
| `time_window` | A window on another date or UTC offset than the shifts (`impossible`), or one too short to reach between the other stops (`blocked`) | `time_windows[].from`, `.to`, `shifts[].from`, `.to` | [Time windows](https://docs.solvice.io/guides/vrp/v3/constraints.md#time-windows) |
| `shift_end` | The route would finish after `to`, including a task with no window that does not fit in the shift at all | `shifts[].to`, `shifts[].max_overtime_s`, `service_duration_s` | [Shift hours and overtime](https://docs.solvice.io/guides/vrp/v3/constraints.md#shift-hours-and-overtime) |
| `max_distance`, `max_route_duration`, `max_stops`, `max_leg_travel` | A route or leg cap set below what the geography needs | `vehicles[].limits`, `shifts[].limits` | [Route limits](https://docs.solvice.io/guides/vrp/v3/constraints.md#route-limits) |
| `period_limit`, `quota` | A daily, weekly or group cap already used up by the other shifts | `vehicles[].limits.periods[]`, `quotas[]` | [Period limits](https://docs.solvice.io/guides/vrp/v3/constraints.md#period-limits), [Quotas](https://docs.solvice.io/guides/vrp/v3/constraints.md#quotas) |
| `same_vehicle`, `same_day`, `same_route`, `compatible_vehicles` | A relation pins the task to a vehicle, day or route that cannot take it | `relations[]`, and the partner task's own rules | [Relations](https://docs.solvice.io/guides/vrp/v3/constraints.md#relations) |
| `all_or_none`, `synchronized`, `time_lag` | A group member does not fit, or the crew's shifts and windows do not overlap | The members' `time_windows`, `eligible_vehicles`, `shifts[]` | [Relations](https://docs.solvice.io/guides/vrp/v3/constraints.md#relations) |
| `sequence`, `tag_order`, `shipment`, `break`, `multi_trip` | That ordering, pairing, break or reload rule refused every position | The relation, `shipments[]`, `shifts[].breaks[]`, `shifts[].reloads[]` | [Shipments](https://docs.solvice.io/guides/vrp/v3/constraints.md#shipments), [Driver breaks](https://docs.solvice.io/guides/vrp/v3/constraints.md#driver-breaks), [Multi-trip reloads](https://docs.solvice.io/guides/vrp/v3/constraints.md#multi-trip-reloads) |
| `unknown` | The solver recorded no specific rule | Every rule on the task against each vehicle that could serve it | [Reasons](https://docs.solvice.io/guides/vrp/v3/errors.md#reasons) |

New codes can be added as new constraints ship. Branch on the ones you handle and treat the rest as `unknown`.

**3. Apply a relaxation literally, then re-solve**

When `relaxations[]` is non-empty, it names the field and the amount: above, raising `van-1`'s `capacity.weight` by 15, or lowering `job-heavy`'s `pickup_load.weight` by 15, is enough. The hint is best-effort and about this task alone, so re-solve and read `unassigned[]` again.

**4. Evaluate a plan you built by hand**

When a reason code is not enough, force the placement and let evaluate say exactly what breaks. Send the same `problem` with a `plan` that puts the task where you expect it:

```json theme={null}
{
  "plan": {
    "routes": [
      {
        "vehicle": "van-1",
        "shift": "mon",
        "stops": [
          { "type": "job", "id": "job-ok" },
          { "type": "job", "id": "job-early" }
        ]
      }
    ]
  }
}
```

The stop excerpt under [On evaluate](#on-evaluate-typed-violations) is this call's answer: `job-early`'s window closes at 08:05, the van arrives at 08:11:38, so the violation is `time_window` with `excess: 398` seconds. That number is the amount to widen the window by, or the service time to shave off `job-ok`. Evaluate does not search, so the answer carries no search noise.

**5. Ask suggest for the cheapest slot**

To price a task against a plan you already have, send `problem` and `plan` to `/v3/routing/suggest` with the task left out of `plan`. Each feasible insertion carries its cost delta, and a task with no feasible position says so:

```json theme={null}
{
  "suggestions": [
    {
      "task": "job-later",
      "insertions": [
        {
          "delta": { "cost": 7.30516, "distance_m": 1631, "travel_s": 118 },
          "placements": [
            {
              "type": "job",
              "id": "job-later",
              "vehicle": "van-1",
              "shift": "mon",
              "arrival": "2026-07-01T08:00:59+02:00",
              "service_start": "2026-07-01T08:00:59+02:00",
              "latest_arrival": "2026-07-01T11:37:19+02:00"
            }
          ],
          "routes": [
            {
              "vehicle": "van-1",
              "shift": "mon",
              "stops": [
                { "type": "job", "id": "job-later" },
                { "type": "job", "id": "job-ok" }
              ]
            }
          ]
        }
      ]
    },
    {
      "task": "job-heavy",
      "insertions": [],
      "reason": { "code": "infeasible", "message": "No feasible insertion position found" }
    }
  ],
  "truncated": false,
  "timings": { "total_ms": 0 },
  "warnings": []
}
```

`latest_arrival` is the slot's slack: `job-later` can arrive as late as 11:37 and the route still holds. Only `reason.code: "infeasible"` is final. See [When a task gets no insertions](https://docs.solvice.io/guides/vrp/v3/suggest.md#when-a-task-gets-no-insertions).

**6. Change one thing at a time, with the seed fixed**

Keep `options.seed` constant between runs, so a difference comes from your change and not from a different random walk, and compare in the solver's order: lower `score.unassigned_priority` first, then lower `score.cost`. A 90-minute shift with three 50-minute jobs:

| Change | `tasks.served` | `unassigned[].constraint` | `time.overtime_s` |
| - | - | - | - |
| None | 1 | `shift_end` on two jobs, category `blocked` | 0 |
| `max_overtime_s: 3600` on the shift | 2 | `shift_end` on one job | 796 |

Had the second run also widened a window, you could not tell which change placed the second job. See [Comparing runs](https://docs.solvice.io/guides/vrp/v3/concepts/solution-quality.md#comparing-runs).

## Fix

Ranked by how little they change about the operation. `impossible` and `blocked` tasks need 1 to 3, `priced_out` needs 4, and only `time_limit` needs 5.

### 1. Relax the rule

**Soften one window.** Field: `time_windows[].per_late_hour`, on a task with exactly one window.

```json theme={null}
{
  "id": "job-early",
  "location": [3.73, 51.045],
  "service_duration_s": 600,
  "time_windows": [
    { "from": "2026-07-01T08:00:00+02:00", "to": "2026-07-01T08:05:00+02:00", "per_late_hour": 60.0 }
  ]
}
```

Trade-off: a late arrival becomes a cost in `components.lateness` instead of an unassigned task, and nothing in `unassigned[]` or `violation_count` flags it; watch `stops[].lateness_s`. `objective.per_late_hour` softens every window at once, at the same loss of visibility.

**Widen the window** by the `relaxations[].amount` the solver gave you. Field: `time_windows[].to` (or `.from`).

```json theme={null}
{
  "id": "job-afternoon",
  "location": [3.71, 51.06],
  "time_windows": [
    { "from": "2026-07-01T10:00:00+02:00", "to": "2026-07-01T12:00:00+02:00" }
  ]
}
```

Trade-off: a promise to the customer changes. The rule stays hard, so the response still reports when it cannot be kept.

**Make the task optional.** Field: `drop_cost`, in your currency. It cannot be combined with `priority`.

```json theme={null}
{
  "id": "job-optional",
  "location": [3.95, 51.20],
  "service_duration_s": 300,
  "drop_cost": 40.0
}
```

Trade-off: the task is served only while serving it costs less than 40; otherwise it comes back as `priced_out` and leaves `unassigned_required` untouched. Set the fee to what a missed visit costs you: a run with `1.0` dropped the task rather than drive a 45 km round trip.

When every task must stay required, rank them with `priority` instead, so the solver chooses which to keep when not all fit. See [Priority](https://docs.solvice.io/guides/vrp/v3/concepts/scoring-explanation.md#priority-which-tasks-are-kept-when-not-all-fit).

### 2. Add capacity

**A bigger or an extra vehicle.** Field: `vehicles[].capacity`, or a new `vehicles[]` entry.

```json theme={null}
{
  "id": "van-2",
  "capacity": { "weight": 30 },
  "shifts": [
    { "id": "mon", "from": "2026-07-01T08:00:00+02:00", "to": "2026-07-01T12:00:00+02:00" }
  ]
}
```

Trade-off: vehicles are free by default, so the solver uses the new one whenever that saves anything. Price it with `costs.per_route` if it should open only when needed.

**More working time.** Field: `shifts[].max_overtime_s`, priced with `costs.per_overtime_hour`.

```json theme={null}
{
  "id": "mon",
  "from": "2026-07-01T08:00:00+02:00",
  "to": "2026-07-01T09:30:00+02:00",
  "max_overtime_s": 3600,
  "costs": { "per_overtime_hour": 50.0 }
}
```

Trade-off: the grant is hard at `to + max_overtime_s`, so `shift_end` can still refuse a task. Without the rate there is no overtime premium, only the `per_route_hour` charge on those hours, so the solver uses all of it: the run above ran 796 seconds over with `components.overtime` at `0.0`. A second `shifts[]` entry on another date adds working time without a new vehicle.

### 3. Split the task

Field: two `jobs[]` entries in place of one, each with part of the load or the service time.

```json theme={null}
[
  { "id": "job-heavy-a", "location": [3.73, 51.045], "service_duration_s": 300, "pickup_load": { "weight": 10 } },
  { "id": "job-heavy-b", "location": [3.73, 51.045], "service_duration_s": 300, "pickup_load": { "weight": 10 } }
]
```

Trade-off: two visits, two service times and two stops against `max_stops`. Add a `same_route` relation when both halves must travel together, or leave them free so two vehicles can share the work. A load no single vehicle can carry is the one `impossible` case this fixes without touching the fleet.

### 4. Change what the solver trades off

For `priced_out`, raise the `drop_cost` until serving is the cheaper option, or remove it to make the task required. When vehicles carry a `per_route`, a dropped task may be paying for the route it would open alone: compare the fee against that rate.

```json theme={null}
{
  "objective": {
    "currency": "EUR",
    "costs": { "per_route": 150.0, "per_travel_km": 0.20, "per_route_hour": 35.0 }
  }
}
```

Trade-off: the objective is fleet-wide, so lowering `per_route` for one task also opens vehicles elsewhere. Rates never rescue a task a hard rule refuses: `impossible` and `blocked` tasks stay out whatever the costs say.

### 5. Raise `time_limit_s`

Field: `options.time_limit_s`, at most `60` on `POST /v3/routing/solve` and `3600` on an async run. Only for `category: "time_limit"`.

```json theme={null}
{
  "options": { "time_limit_s": 10, "seed": 42 }
}
```

Trade-off: latency. The default is 150 ms plus 4 ms per task. Read `search_progress` to see where the curve flattens, and try another `seed` before buying more time. See [The time budget](https://docs.solvice.io/guides/vrp/v3/concepts/performance-guide.md#the-time-budget).

> [card] [Errors](https://docs.solvice.io/guides/vrp/v3/errors.md#unassigned-jobs)
>
> Every category and reason code, with what to check.

> [card] [Evaluate](https://docs.solvice.io/guides/vrp/v3/evaluate.md)
>
> Score a plan you built and read its typed violations.

> [card] [Constraints](https://docs.solvice.io/guides/vrp/v3/constraints.md)
>
> Every hard and soft rule, with the JSON to send.

> [card] [Solution quality](https://docs.solvice.io/guides/vrp/v3/concepts/solution-quality.md)
>
> Coverage, cost and how to compare two runs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.