Staffing Curve

Erlang C in Power BI: the full calculation, and why DAX fights you

For workforce analysts who already have the forecast in Power BI and want the staffing requirement next to it. Every formula, every rule, and an honest account of where each approach breaks.

1. The calculation, end to end

Seven inputs, one integer out. For one interval:

SymbolMeaningTypical value
VContacts offered in the intervalfrom the forecast
AHTAverage handle time, secondssee section 4 — this must be weighted
IInterval length, seconds900, 1800 or 3600
SLtargetService-level target0.80
TAnswer threshold, seconds20
SShrinkage0.30
OmaxMaximum occupancy0.85

Offered load

A = V × AHT ÷ I

A is traffic intensity in Erlangs — the average number of agents that would be busy at any instant if nobody ever queued. 120 contacts at 300 seconds in a half-hour is 20 Erlangs.

Erlang B

Erlang C is derived from Erlang B, the probability that all N agents are busy in a system with no queue. The textbook form is

B(N) = (Aᴺ ÷ N!) ÷ Σₖ₌₀..ₙ (Aᵏ ÷ k!)

Don't use it. Use the recurrence, which is algebraically identical and never leaves the interval (0, 1]:

B(0) = 1
B(n) = A × B(n−1) ÷ (n + A × B(n−1))

section 3 explains why this matters more than it looks.

Erlang C — the probability of waiting

For N > A, with ρ = A ÷ N:

C = B(N) ÷ (1 − ρ + ρ × B(N))

For N ≤ A there is no steady state: arrivals outpace service indefinitely and the queue grows without bound. The formula's assumptions don't hold, and the interval should be reported as overloaded, not run through arithmetic that has stopped meaning anything.

Service level

SL(N) = 1 − C × exp(−(N − A) × T ÷ AHT)

The share of contacts answered within T seconds, given N productive agents.

Occupancy

Occupancy = A ÷ N

The share of paid productive time actually spent handling contacts.

Required productive agents

The smallest integer N that satisfies all three of:

N > A                 the queue has a steady state
A ÷ N ≤ O_max         the staffing level is sustainable
SL(N) ≥ SL_target     the service target is met

The third condition is the one everyone implements. The second is the one that separates a number that clears the target on paper from one a team can work. section 5 is about that.

Shrinkage — applied last

Scheduled = ceil(N ÷ (1 − S))

Shrinkage grosses up the productive requirement to a scheduled headcount. It is applied after Erlang C, never to the offered load, never to the handle time, never inside the formula. Folding it into A is a common shortcut and it is wrong: it changes the shape of the queue, not just the headcount.

Everything in section 1 except the last step is a closed formula. DAX can express closed formulas. The last step is a search — start at a candidate N, evaluate the service level, and if it falls short, increment and try again. DAX has no loop.

The standard workaround is to evaluate the service level for every candidate at once and take the minimum that passes:

Required Productive Agents =
VAR A   = [Offered Load]
VAR AHT = [Weighted AHT]
VAR T   = 20
VAR Tgt = 0.80
VAR Candidates =
    ADDCOLUMNS (
        GENERATESERIES ( 1, 300, 1 ),
        "SL",
            VAR N = [Value]
            VAR B =
                DIVIDE (
                    POWER ( A, N ) / FACT ( N ),
                    SUMX ( GENERATESERIES ( 0, N, 1 ), POWER ( A, [Value] ) / FACT ( [Value] ) )
                )
            VAR C = DIVIDE ( B, 1 - A / N + A / N * B )
            RETURN IF ( N > A, 1 - C * EXP ( - ( N - A ) * T / AHT ), 0 )
    )
RETURN
    MINX ( FILTER ( Candidates, [SL] >= Tgt ), [Value] )

This works, for a while. Three things are wrong with it:

None of this is a criticism of DAX. It is a query language for aggregating columns, and this is an iterative numerical search. The mismatch is structural.

3. The overflow at 170 agents

170! is about 7.3 × 10³⁰⁶. 171! is Infinity in a double, and Aᴺ for large A gets there sooner. Once either term overflows, Erlang B evaluates to NaN and everything downstream — C, service level, the whole search — is NaN too.

In contact-centre terms: the factorial form fails at roughly 1,020 contacts in a half-hour at a 300-second AHT. That is a busy interval, not an absurd one. Any large operation crosses it every day.

The recurrence in section 1 never overflows, because every intermediate value is a probability. At extremely low load and very high agent counts it underflows to exactly 0, which is the correct limit and stays benign: service level then evaluates to 1, not NaN.

You cannot write the recurrence in a DAX measure, for the reason in section 2. You can write it in Power Query's M, which does have List.Accumulate — but then the requirement is computed at refresh time, for fixed parameters, and a what-if slicer can't move it. That trade-off is real and worth knowing before you choose.

4. The mistake that looks fine: averaging handle time

This one is not about Erlang C at all. It is about Power BI aggregation, and it is the most common source of a wrong answer in the field.

If your table stores an average handle time per row and you drop that column into a measure as AVERAGE, Power BI averages the averages across whatever grain the visual is showing. An interval with 5 contacts at 900 seconds and one with 200 contacts at 240 seconds averages to 570 — when the true weighted handle time is 256. Offered load comes out more than double, and so does the staffing requirement. Nothing errors. The chart looks plausible. The number is wrong.

Store handle seconds, not the average, and write the measure that weights correctly:

Weighted AHT =
DIVIDE ( SUM ( Intervals[HandleSeconds] ), SUM ( Intervals[Contacts] ) )

This is the single check worth making before trusting any Erlang C output in Power BI, from any tool including ours.

5. Why service-level-only answers are too low

Take 60 Erlangs — 360 contacts in a half-hour at a five-minute AHT — with an 80% in 20 seconds target.

A search that stops at the first N meeting service level returns 67 agents. Occupancy at 67 is 60 ÷ 67 = 89.6%. That headcount clears the target and burns the team out: sustained occupancy above roughly 85% drives fatigue, error rates and attrition, and every experienced planner knows it.

Enforce an 85% ceiling as a hard constraint alongside service level and the answer is 71. At 30% shrinkage that is 102 scheduled agents against 96 — six more people on the roster, every interval like this one, because the naive answer was never sustainable.

The ceiling is not a fudge factor. It is the constraint that makes the number achievable, and it belongs inside the search, not in a footnote. Occupancy falls monotonically as N grows, so the search can start at max(floor(A) + 1, ceil(A ÷ O_max)) — which satisfies the first two conditions by construction — and only ever increment for service level. The two constraints can never be traded off against each other.

This is why our numbers come out higher than most online calculators. Try one at erlangcalc.com — it shows both answers side by side for whatever inputs you give it.

6. The three ways people do it today

A DAX measure

The approach in section 2. Interactive, respects slicers, no external dependencies. Breaks at 170 agents, slow over a date axis, and the candidate ceiling is a guess. Right answer for a small operation that wants a single number on a card.

A Python or R visual

Loops, no overflow, any library you like. But: Python visuals are disabled by policy in many tenants, they don't render in the Power BI mobile app or in embedded scenarios, they re-execute on every interaction with a visible delay, and your contact data is handed to a script runtime that the report reader can read. Right answer for an analyst's own workbench, wrong answer for a shared report.

Export to Excel

The one most people actually do. The forecast lives in Power BI, the Erlang C lives in a spreadsheet someone built years ago, and the two are reconciled by hand every week. Nothing about this is wrong except everything that goes wrong when the spreadsheet drifts from the report.

A custom visual

Which is what we built. The Erlang C Staffing Planner runs the recurrence, the search and the occupancy constraint in TypeScript inside the visual, on the report reader's machine. No network requests, no script runtime, renders everywhere Power BI does. Thirty thousand intervals in under a tenth of a second. Every interval's tooltip shows the full chain — offered load, required productive, required scheduled — so any number can be checked against the spreadsheet you're replacing. It is licensed per user through Microsoft and the evaluation tier is free and permanent.

7. Edge cases that need a decision, not a formula

These are choices. The maths runs out and something has to be decided. Whatever you use, it should document its answer to each.

CaseOur ruleWhy
No contacts in the intervalRequired staff 0; service level and occupancy not applicableReporting 100% would count an overnight interval as a service-level success and inflate the day's summary.
Contacts but zero AHTInput errorHandle time of zero against real volume is a data problem, not a staffing answer.
Scheduled agents ≤ offered loadOverloaded; service level reported as 0The queue has no steady state. Zero is a flag meaning "unservable at this roster", not a forecast.
Target above 99.9%RejectedService level approaches 100% but never reaches it. A 100% target is only "met" when the exponential underflows the double — an artefact of precision, not a staffing result.
Exact staffingReports as exact63 productive → 90 scheduled at 30% shrinkage, but 90 × 0.7 = 62.999999999999993 in binary. Without a tolerance the interval reports one agent short for staffing that is exactly right. It happens 143 times below 5,000 agents.

8. What Erlang C assumes

Poisson arrivals, exponentially distributed handle times, no abandonment, and infinite caller patience. Real queues abandon, which makes Erlang C generally conservative — it recommends slightly more staff than a model with abandonment would. That is usually the safer direction for a plan, but it should be said out loud to anyone using the numbers.

Out of scope for Erlang C entirely: multi-skill routing, callbacks and retrials, chat concurrency, and anything that needs simulation. If your operation is dominated by those, Erlang C is a floor, not an answer.

The calculation above, on every interval in your report

The Erlang C Staffing Planner is in Microsoft's certification queue for AppSource. When it lands, Evaluation Mode is free and permanent — put your own data through it and check the tooltips against your spreadsheet before you decide anything. Until then, the free calculator runs the same engine on one interval.

Try one interval free   See pricing