Even by construction
The solver minimizes the load range across bins, so no bin is left carrying twice its neighbour. Round-robin balances counts; aequitas balances weight.
Give it weighted items and bins with capacities. It returns an even, in-band, preference-respecting layout. Deterministic, pure, zero dependencies.
npm install aequitasBoth ESM and CommonJS ship in the package:
import { suggest, rebalance, cost, rankToAffinity } from "aequitas"; // ESM / TypeScript
const { suggest, rebalance, cost, rankToAffinity } = require("aequitas"); // CommonJSFour functions. Two of them are the ones you will actually call.
Here the bins are lecturers with a credit-hour cap, and the items are course sections weighted by their credit hours.
import { suggest, rebalance, cost, rankToAffinity } from "aequitas";
const lecturers = [
{ id: "jane-doe", max: 12 },
{ id: "john-doe", max: 12 },
{ id: "mary-major", max: 12 },
];
const sections = [
{ id: "capstone-project", weight: 5 },
{ id: "algorithms", weight: 4 },
{ id: "databases", weight: 3 },
{ id: "networks", weight: 3 },
{ id: "database-lab", weight: 2 },
];
const plan = suggest(sections, lecturers);
plan.loads; // → { "jane-doe": 5, "john-doe": 6, "mary-major": 6 }
plan.violations; // → 0 everyone inside their band
plan.unassigned; // → []
// Next term someone goes on sabbatical, or a section is added. Improve the
// timetable you already have instead of starting over, holding anything pinned.
const revised = rebalance(sections, lecturers, plan.assignments);
// Score any layout with the exact function the solver minimizes.
cost(sections, lecturers, plan.assignments); // → 1
// Turn a 1-based preference rank into an affinity score.
rankToAffinity(1); // → 1
rankToAffinity(2); // → 0.5Round-robin balances the number of things per bin. Almost nothing you actually assign is uniform: course sections differ in credit hours, tickets differ in effort, shards differ in request rate. Balance the count and you leave the weight lopsided.
Dealing those five sections out round-robin gives { "jane-doe": 8, "john-doe": 6, "mary-major": 3 }. Jane ends up teaching nearly three times Mary's load, having been handed the same number of courses.
Balancing the credit hours instead gives { "jane-doe": 5, "john-doe": 6, "mary-major": 6 }, and it does so while respecting each lecturer's cap, honouring anything the department has already promised, and keeping requested courses as a tiebreaker rather than a rule.
That is the whole job. How it works walks through the cost function and the two-phase solver that gets there.
Lecturers and course sections are the running example throughout these docs because the constraints are easy to state out loud: a minimum and maximum teaching load, who requested which course, who will not co-teach with whom.
Nothing in the library knows about any of that. An item is an id and a weight; a bin is an id and a capacity band. The same two nouns cover tasks onto workers, shifts onto staff, shards onto nodes, and support tickets onto agents.
Use cases works four of those through end to end, taken from Node.js codebases and the teams around them: monorepo CI tasks, test shards, database shards and a support backlog. Every number on those pages came from running the package, including the ones where it falls short of the theoretical best.
If you are looking for the name of the problem you have, this is weighted load balancing over capacity-constrained bins, which the literature splits into bin packing, multiway number partitioning and the generalized assignment problem. Practical versions of it show up as balancing a team's workload, scheduling teaching load, distributing shards evenly, and spreading replicas across failure domains. aequitas solves it heuristically rather than exactly, which how it works explains and justifies.
engines.node is >=22)