What it means
Round-robin assignment is the oldest scheduling idea in computing applied to salespeople. Keep a list, keep a pointer, hand each new item to whoever the pointer is on, then advance the pointer and wrap around at the end. In a CRM it is usually the last rule in a lead routing chain: when no territory, segment or ownership rule applies, distribute evenly.
The appeal is that it is auditable. Any rep can count their own leads and check, which matters more than it sounds, because distribution disputes inside a sales team are corrosive and nearly impossible to settle with a black-box model. Round-robin's fairness claim is narrow and verifiable: equal counts over a long enough window.
That narrowness is also its weakness. Equal counts are only fair when the items are interchangeable, and inbound leads rarely are. Everything else on this page is about the two questions the naive version cannot answer: what happens when owners have different capacity, and what happens when the leads have different value.
The formulas, written out
Plain round-robin. With k eligible owners and n as the zero-based sequence number of the record:
owner = eligible[n mod k]
eligible: the ordered list of owners who can receive this record right now, after availability and capacity filters.k: the length of that list.n: a persisted, monotonically increasing counter.
Weighted round-robin, deficit formulation. This is the version worth implementing. For each owner i, before assigning record number T + 1:
deficit_i = ( w_i / sum of all w ) x (T + 1) - a_i
Assign to the owner with the largest deficit.
w_i: capacity weight of owner i.a_i: how many records owner i has already received in this cycle.T: total records already assigned.deficit_i: how far behind their fair share owner i currently is. Negative means they are ahead.
Ties are broken by higher weight, then by a stable owner ID so the result is reproducible. This formulation is worth the extra arithmetic because it interleaves owners naturally instead of producing blocks, it handles roster changes without reshuffling anybody, and it makes the fairness claim explicit: nobody is ever more than one record away from their proportional share.
A worked example, end to end
Three reps with weights Ada 3, Bora 2, Cem 1. Total weight 6, so the target shares are 50%, 33.3% and 16.7%.
Walking the first six assignments through the deficit formula:
- Record 1. Deficits are 0.500, 0.333, 0.167. Ada wins. Counts (1, 0, 0).
- Record 2. Ada is now at
0.5 x 2 - 1 = 0.000, Bora at0.333 x 2 - 0 = 0.667, Cem at 0.333. Bora wins. Counts (1, 1, 0). - Record 3. Ada 0.500, Bora 0.000, Cem 0.500. Tie between Ada and Cem, broken by weight. Ada wins. Counts (2, 1, 0).
- Record 4. Ada 0.000, Bora 0.333, Cem 0.667. Cem wins. Counts (2, 1, 1).
- Record 5. Ada 0.500, Bora 0.667, Cem -0.167. Bora wins. Counts (2, 2, 1).
- Record 6. Ada 1.000, Bora 0.000, Cem 0.000. Ada wins. Counts (3, 2, 1).
After six records the split is exactly 3, 2, 1, and the order was Ada, Bora, Ada, Cem, Bora, Ada. Compare that with the naive weighted approach of repeating names in a list, which produces Ada, Ada, Ada, Bora, Bora, Cem: the same totals, but Cem waits through five assignments before seeing anything, and on a slow day that is the difference between a lead and no lead.
Extend to 40 records with plain, unweighted round-robin across five reps and everyone gets 8. Now take Cem out for three days in the middle of that period. If the rotation simply skips him, the distribution ends at 9, 9, 8, 8, 6. If the rotation is deficit-based, Cem's deficit accumulates while he is away and he receives a slightly heavier share on return, landing at 8, 8, 8, 8, 8 over a long enough window. Neither behaviour is wrong, but only one of them is a decision.
Three ways it goes wrong
Resetting the pointer every day. This is the most common implementation bug and it is invisible in a single day's data. Five reps, seven leads per day, pointer reset to index 0 each morning. The daily sequence is 0, 1, 2, 3, 4, 0, 1, so reps 1 and 2 get two leads and reps 3, 4 and 5 get one. Over a five-day week that is 10, 10, 5, 5, 5 out of 35 leads, against a fair share of 7 each. Over 250 working days, reps 1 and 2 receive 500 leads each while reps 3, 4 and 5 receive 250. That is a doubling of pipeline input from an algorithm chosen because it was fair. The same bug appears whenever the counter is held in application memory, since every deployment restarts it.
Distributing count instead of value. Take 40 leads split evenly, 8 each across five reps. Suppose 6 of the 40 are enterprise enquiries averaging 22,000 and the other 34 average 3,200. Round-robin does not know the difference, so the enterprise leads land wherever the rotation happens to be. Expected enterprise leads per rep is 1.2, but the chance that any individual rep receives three or more is about 10%, so across a five-person team it is common for one rep to get three and another to get none. Their pipeline values are 82,000 and 25,600, a 3.2x gap produced entirely by the sequence in which forms were submitted. Bucket by segment first, then round-robin inside the bucket.
Not handling declines. A rep marks a lead "not mine" and it re-routes. If the counter is not corrected, that rep has effectively cherry-picked: they receive their full share and hand back the ones they do not want, so the others absorb the difference. If the counter is corrected too aggressively, declining becomes free and the decline rate climbs. The workable rule is to return the record to the pool, keep the decliner's count incremented, and report decline rate per rep as its own metric so the behaviour is visible.
How round-robin is actually implemented in a CRM
Four pieces, and the order of operations matters:
- An eligibility filter evaluated at assignment time: is the owner active, inside working hours in their own timezone, not on declared leave, and below their open-lead cap? Only the survivors enter the rotation.
- A persisted counter or deficit table, updated in the same database transaction as the assignment. Not in memory, not in a cache.
- An atomic increment. Two webhooks arriving in the same millisecond will otherwise read the same pointer value and assign both records to the same owner. In practice this means a database sequence, a row lock, or an update that returns the new value in one statement.
- An audit row per assignment with the owner, the counter value used, the eligible-list size and the timestamp. Without it, "the rotation is broken" is an unfalsifiable claim.
Edge cases that decide whether anyone trusts it:
- Roster changes mid-cycle. Modulo arithmetic reshuffles when
kchanges, so the person who was next is no longer next and the audit trail becomes hard to explain. Deficit tracking survives this without any special handling. - Timezone-spread teams. A rotation that ignores working hours will assign the 03:00 lead to someone asleep. A rotation that respects working hours will systematically under-assign to whichever timezone gets the least traffic, so their deficit grows and the algorithm compensates during their day. That second behaviour is usually what you want, and it only exists if deficits are tracked across days.
- Bulk imports. Uploading 3,000 records runs the rotation 3,000 times in a few seconds and floods every queue. Bulk operations should either bypass the rotation and distribute by an explicit split, or be rate limited.
- Reassignment after the fact. When a manager moves a lead from Ada to Bora, do the counters move too? If not, Ada keeps the credit and Bora does the work. Decide, and apply it in the audit table rather than by silently editing counters.
- Deactivated owners. A departed rep left in the eligible list receives one lead in every rotation and it goes nowhere. Deactivation must remove them from the list and re-route their open records in the same operation.
Why it matters
Distribution is one of the few things in a sales organisation where perceived unfairness does direct damage. A rep who believes the good leads go elsewhere works the queue with less energy, and that belief is very hard to disprove without an audit trail. Round-robin's real product is not the distribution, it is the ability to show anyone exactly why they got what they got.
The second reason is speed. A shared pool that nobody owns has no clock attached to it, and first response time degrades quietly. Assigning immediately, even imperfectly, creates an owner, an expectation and a measurable service-level agreement in the same instant.
Common mistakes
- An in-memory pointer. Resets on deploy, and breaks entirely with more than one application instance.
- Repeating names to express weights. Produces blocks rather than an interleave, so low-weight owners wait a long time between records.
- Rotating before filtering. Pick the owner from the eligible list, not from the full roster with a skip afterwards, or your counter and your assignments disagree.
- Ignoring value. Equal counts with unequal leads is a fairness claim that will not survive one quarter of commission statements.
- No decline metric. Cherry-picking is invisible unless you measure it directly.
Related concepts
- Lead routing: the rule chain round-robin usually sits at the end of.
- First response time: the metric an immediate assignment protects.
- SQL: the acceptance decision the assigned rep has to make.
- Pipeline velocity: uneven distribution shows up here as uneven opportunity counts.
- ICP: the segmentation that should bucket leads before the rotation runs.
- Omnichannel CRM: the same rotation has to work across form fills, DMs and email.
How Pinlyx handles it
Pinlyx keeps rotation state in the database and increments it inside the assignment transaction, so concurrent arrivals from a web form, a Telegram bot and a WhatsApp webhook cannot collide on the same owner. Eligibility is evaluated before the rotation rather than as a skip afterwards, weights are per pipeline, and every assignment writes an audit row with the owner, the rule that produced them and the size of the eligible list at that moment.