Schedule jobs to minimize makespan in Python

A recurring scheduling problem: you have a set of jobs with known durations and several identical machines (or workers, cores, servers), and you want to split the jobs so everything finishes as early as possible. The finish time of the busiest machine is the makespan, and minimizing it is the classic parallel-machine scheduling problem. This is load balancing across machines — the P || Cmax problem — not job-shop or staff rostering.

The obvious approach is to try every way of assigning jobs to machines. The approach that scales is to model it and hand it to a solver — with quicopt that is a few lines of Python.

The brute-force trap

"Try every assignment" means looping over all ways to place N jobs on M machines:

brute_force.py
from itertools import product

proc = [3, 1, 6, 4, 2, 5, 7, 2]   # processing time per job
M = 3
N = len(proc)

def makespan(assign):
    load = [0] * M
    for j, m in enumerate(assign):
        load[m] += proc[j]
    return max(load)

print(min(makespan(a) for a in product(range(M), repeat=N)))  # -> 10

Fine for eight jobs on three machines. But the number of assignments is M^N, which explodes:

  • 8 jobs, 3 machines → 6,561 (instant)
  • 20 jobs, 3 machines → 3,486,784,401
  • 20 jobs, 5 machines → 95,367,431,640,625

Enumeration is hopeless the moment the shop is real.

Model it as a MILP

Makespan minimization is a mixed-integer linear program (MILP). A binary variable x[j][m] puts job j on machine m. Each job goes on exactly one machine; one continuous variable makespan is forced to be at least the load of every machine; and we minimize it:

scheduling.py
from ortools.math_opt.python import mathopt
from quicopt import Client

# Distribute N jobs across M identical machines to minimize the makespan
# (the finish time of the busiest machine).
proc = [3, 1, 6, 4, 2, 5, 7, 2]   # processing time per job
M = 3
N = len(proc)

model = mathopt.Model(name="scheduling")
x = [[model.add_binary_variable(name=f"x_{j}_{m}") for m in range(M)] for j in range(N)]
makespan = model.add_variable(lb=0.0, name="makespan")

for j in range(N):                                   # each job on exactly one machine
    model.add_linear_constraint(sum(x[j][m] for m in range(M)) == 1)
for m in range(M):                                   # makespan >= load of every machine
    model.add_linear_constraint(sum(proc[j] * x[j][m] for j in range(N)) <= makespan)

model.minimize(makespan)

client = Client("https://try.quicoptapi.pgi.fz-juelich.de")
result = client.solve(model)
print(result.display)
$ python scheduling.py
├── status:     optimal
├── feasible:   true
├── objective:  10.0
├── x:          makespan=10, x_0_0=0, x_0_1=0, x_0_2=1, x_1_0=0, x_1_1=1, …  (25 variables)
└── solve_time: 0.0196 s

What you get

status: optimal means the solver proved the makespan can't go below 10. The total work is 30 units over three machines, so 10 is a perfect balance — and the solver found an assignment that hits it, in a fraction of a second.

The difference is scaling. Brute force is O(M^n); a MILP solver prunes the search with bounds instead of enumerating it. The same model that schedules 8 jobs schedules 800 — you change the data, not the approach. (This is the P || Cmax problem, NP-hard in general, which is exactly why a solver beats hand-rolled search.)

Next

Reference: R. L. Graham, Bounds on Multiprocessing Timing Anomalies, SIAM Journal on Applied Mathematics, 1969.

A bigger schedule — or a different problem?

Tell us what you're optimizing. We'll help you model it and point you at the right approach.

Talk to us →

Frequently asked questions

Is this job-shop scheduling?

No — this is parallel-machine makespan (P||Cmax). Job-shop, with per-machine operation sequences and precedence, is a different and larger MILP.

Is this the same as employee shift scheduling?

No — that's workforce rostering (covering demand with staff); see the shift-scheduling guide. This one balances jobs across machines to finish early.

How do I add release times or precedence?

As more linear constraints on the same model — a job can't start before its release time, or must finish before another begins.

Is quicopt free to use?

Yes — pip install quicopt and your first call sets up a free key, no license.