[Canonical page](<https://docs.dhtmlx.com/planning-engine/resource-planning/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Resolving resource overload

Resource leveling changes when work happens so that assignments compete less for limited capacity. Planning Engine can delay tasks, pause work that's already started, and reschedule dependent tasks around those changes.

It doesn't change who's assigned, though. If the better answer is to give the work to someone else, use [assignment suggestions or alternative evaluation](<https://docs.dhtmlx.com/planning-engine/planning-alternatives.md>).

## Why a scheduled plan can still be overloaded

In the [delivery example](<https://docs.dhtmlx.com/planning-engine/modeling.md#build-the-example-plan>), Design finishes Monday at noon. Implement and Write guide both start at 13:00, and both need Ann at full capacity.

The dependencies allow this overlap. Ann's capacity doesn't: two assignments of 1 give load 2 against capacity 1 for Monday afternoon.

A dependency schedule sets the order of work. A resource-feasible plan also has to account for who can do that work at the same time. The engine reports the contention even before leveling, so your application can show it, ask for a decision, or resolve it automatically.

Here's the same situation on a bigger plan. Dependencies allow tasks to overlap, and the load rows under the chart flag each person who's booked twice:

![A Gantt chart with per-person load rows underneath; red windows mark days where a person's load reaches 2 against a capacity of 1](<https://docs.dhtmlx.com/planning-engine/assets/level-resources-before.webp>)

## Reading load and capacity

A resource load profile compares booked load with capacity over time. In the example, Ann's Monday profile looks like this:

| Working period | Booked work | Load | Capacity |
|---|---|---|---|
| 08:00-12:00 | Design | 1 | 1 |
| 13:00-17:00 | Implement + Write guide | 2 | 1 |

An **overallocation window** groups consecutive overloaded time buckets. For each window, the engine reports the contributing tasks, peak load, capacity, and total excess. With hourly detection, four buckets at load 2 and capacity 1 add up to an overload of 4 capacity-hours.

A load profile maps naturally onto a grid of people by buckets. In this demo, each cell is one day bucket with its booked load, capacity follows each person's calendar, and the red cells are overloaded:

![A team load grid: one row per person, one cell per day showing booked load, hatched cells for days off, and red cells where load exceeds capacity](<https://docs.dhtmlx.com/planning-engine/assets/resource-load.webp>)

Calendars limit where assignments book work. Task pauses book nothing, and assignment slices book only their selected working portions. In resource-calendar mode, resources with their own calendars also show zero capacity in buckets with no working time.

**API:** `loadProfile` - [Rust](<https://docs.dhtmlx.com/planning-engine/rust/api/project-load-profile.md>) · [WASM](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-load-profile.md>) · [JS](<https://docs.dhtmlx.com/planning-engine/js/api/project-load-profile.md>) · [.NET](<https://docs.dhtmlx.com/planning-engine/net/api/project-load-profile.md>) · [Java](<https://docs.dhtmlx.com/planning-engine/java/api/project-load-profile.md>)

### Choosing the detection granularity

The engine detects overload by hour or by day. By default, it follows the project's duration unit: day for day-based projects, hour for hour- or minute-based ones.

Any assignment with working time inside a bucket adds its full rate to that bucket. That's fine for broad planning, but coarse detection can overstate contention inside a bucket.

Say a person works on one task from 08:00-12:00 and on another from 13:00-17:00. There's no real overlap. Day-based detection still counts both assignments and reports load 2. Hour-based detection keeps those periods apart. The same approximation can still happen within an hour, when two tasks take different half-hours.

Overload totals measure excess rates across buckets. They aren't stored effort, overtime requirements, or a financial cost. Compare them on a consistent basis, and pick a granularity that fits the decisions you're making.

## How leveling changes a plan

Leveling starts with a scheduled plan, finds the earliest overload it can act on, proposes corrections, and reschedules. Moving one task can move its successors or change other resources' load, so the engine checks contention again after the scheduling changes.

![Leveling loop: while there is an overload leveling can still act on, pick the task that gives way, delay or split it, and reschedule dependent work; otherwise return changed dates and any remaining windows](<https://docs.dhtmlx.com/planning-engine/assets/leveling-loop.svg>)

A **delay** moves the whole task later. A **split** keeps earlier work in place, inserts a pause, and resumes the remaining work afterward. Either way, the task keeps its required working duration, though its calendar span can get longer.

In the example, delaying the guide until Ann finishes implementation gives a feasible arrangement:

| Task | Before leveling | After delaying the guide |
|---|---|---|
| Design | Monday morning | Monday morning |
| Implement | Monday afternoon + Tuesday morning | Monday afternoon + Tuesday morning |
| Write guide | Monday afternoon | Tuesday afternoon |
| Review, assigned to Ben | Tuesday afternoon | Tuesday afternoon |
| Ready to release | Tuesday 17:00 | Tuesday 17:00 |

The guide had room to spare before release, and now it uses it. Ann and Ben work on different tasks Tuesday afternoon, so nobody's overloaded and the finish doesn't move.

Splits help when a lower-preference task is already running before another task needs the same person. The engine can keep its earlier work and pause it through the contention. Without splits, fixing that contention may mean moving the whole task, including work that already fit.

A split applies to the whole task: every assignment observes the same pause dates on its own calendar. Leveling doesn't move one person's assignment on its own or change its slice.

Here's the plan from the earlier screenshot after leveling. Hatched blocks are delays, and dashed gaps are pauses inside split tasks. Every load row is back within capacity:

![The same Gantt chart after leveling: two tasks start later behind hatched delay blocks, two tasks are split by dashed pauses, and every person's load row reads within capacity](<https://docs.dhtmlx.com/planning-engine/assets/level-resources-after.webp>)

**API:** `level` - [Rust](<https://docs.dhtmlx.com/planning-engine/rust/api/project-level.md>) · [WASM](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>) · [JS](<https://docs.dhtmlx.com/planning-engine/js/api/project-level.md>) · [.NET](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) · [Java](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>)

## Deciding which work gives way

The engine offers three ordering policies:

| Policy | Preference |
|---|---|
| Input order | Earlier task rows generally stay; later rows move. The policy is called ID order, but it uses input position, not sorted identifier values. |
| Standard | Considers running work, dependency relationships, available slack, current dates, priority, and dated constraints. |
| Priority | Gives task priority precedence, then uses the standard considerations. |

Under standard order, a predecessor is preferred over a successor, because delaying the predecessor can push both. A task with more slack is usually cheaper to delay. Later starts and lower priorities also tend to give way. Input order breaks any remaining ties.

Already-running work gets special treatment. If it can only give way by moving the whole task, the engine normally moves a newcomer first, so it doesn't throw away useful placement from before the conflict. If the running task can be paused, it competes on the other factors like any other task, since its earlier work can stay.

In the example, the guide has more scheduling room than implementation, so it's the natural one to delay. Give the guide higher priority and that preference can flip. Implementation would then need later capacity, which can also push Ben's review and the release milestone.

Manual tasks, protected completed tasks, enabled must-start/must-finish pins, and maximum-priority tasks don't move during leveling. Summaries aren't leveled directly; their dates follow changes to their children. A fixed contender still uses capacity, so other work may have to move around it.

## Controlling the tradeoffs

Leveling controls let your application say which parts of the plan are open to change:

| Choice | Practical effect |
|---|---|
| Select resources | Resolve contention on selected resources while still reporting other overload. |
| Select movable tasks | Allow changes only to selected tasks; all other assignments still contribute load. |
| Select a date range | Act on contention triggered inside that range. Tasks can start before it and can land after it. |
| Allow splits | Keep earlier work where possible by pausing tasks instead of moving everything. |
| Stay within slack | Avoid corrections that need more scheduling room than the task currently has. Unresolved overload can remain. |
| Clear previous leveling | Remove stored delays and pauses in the selected task set before making a fresh attempt. |

Splits and clearing previous leveling are on by default. Clearing applies to the task selection, not just the selected resources or triggering dates. The engine treats all stored pauses as leveling-owned. There's no separate class of user pauses that's protected from clearing.

The within-slack policy measures available room against the project finish set at the start of the run, after clearing. It recalculates room as changes propagate, so slack used up by one task's movement isn't available twice. It doesn't keep pushing the reference finish out to create new slack.

**API:** `LevelingOptions` - [Rust](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/leveling-options.md>) · [WASM](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/leveling-options.md>) · [JS](<https://docs.dhtmlx.com/planning-engine/js/api/types/leveling-options.md>) · [.NET](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md>) · [Java](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-options.md>)

### Small steps or larger jumps

The default policy fixes contention in small steps and reconsiders the selection as the plan changes. The other policy jumps past the currently contended region, which can cut down the number of rounds.

Larger jumps can produce a different arrangement, because the engine doesn't reconsider every bucket in between. If a jump can't fit within slack or can't survive the scheduling constraints, the engine can fall back to a smaller correction.

Use the same policy when you compare a hypothetical plan and when you later commit it. The larger-jump option is a different planning choice, not just a faster way to get the same dates.

## Understanding unresolved overload

Sometimes no allowed correction exists. The engine keeps the remaining windows and explains the main reason:

| Reason | What to examine |
|---|---|
| Pinned constraint | A must-date or another hard scheduling requirement prevents the proposed movement. |
| Fixed task | A manual task or summary can't be leveled directly. |
| Completed | Completed work is protected by progress-aware scheduling. |
| Maximum priority | The task is explicitly protected from leveling. |
| Slack exhausted | No remaining correction fits within the allowed slack. |
| Out of scope | The relevant resource, tasks, or triggering dates were excluded. |

When a hard requirement blocks a proposed change, the engine reverts that correction instead of pushing against the same bound again and again. A failed split can first fall back to a whole-task delay.

A successful leveling run doesn't mean every window is gone. The returned convergence flag tells you whether the date schedule settled. Protected or excluded overload can still remain in a converged plan, so check the final windows, not just the changed dates.

## Previewing and accepting a leveled plan

Leveling applies its changes directly to the project. To compare the result with the existing plan, level an independent copy and inspect its dates and resource profiles before you decide what to adopt.

**How-to:** Preview leveling - [Rust](<https://docs.dhtmlx.com/planning-engine/rust/how-to/preview-leveling.md>) · [WASM](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/preview-leveling.md>) · [JS](<https://docs.dhtmlx.com/planning-engine/js/how-to/preview-leveling.md>) · [.NET](<https://docs.dhtmlx.com/planning-engine/net/how-to/preview-leveling.md>) · [Java](<https://docs.dhtmlx.com/planning-engine/java/how-to/preview-leveling.md>)

The engine levels forward-scheduled projects only. Leveling can extend the finish date, and its deterministic choices don't guarantee the shortest possible project. There's also no final compaction pass that pulls delayed work back into every available gap.

With complex resource calendars and assignment slices, check the current platform limitations before you rely on exact placements. For example, a conflict at the start of a later assignment slice can cause a whole-task delay, even when a split that keeps earlier work would be the better fit.

If moving the guide isn't acceptable, the next question is whether Ben could write it on Monday instead. Continue with [Exploring better plans](<https://docs.dhtmlx.com/planning-engine/planning-alternatives.md>).
