Skip to the content.

Calculation guide

Everything the visual computes, and every rule it applies where the maths runs out. Written to be readable by a workforce analyst, not just by a developer.

Inputs

Symbol Meaning Source
V Forecast contacts offered in the interval Field well
AHT Average handle time, seconds Field well
I Interval length, seconds Format pane
SL_target Target service level, decimal Format pane
T Answer-time threshold, seconds Format pane
S Shrinkage, decimal Format pane
O_max Maximum occupancy, decimal Format pane

Offered load

A = V * AHT / I

A is traffic intensity in Erlangs — the average number of agents busy at any instant if there were no queueing.

Erlang B, and why the recurrence matters

Erlang C is derived from Erlang B. The closed form of Erlang B divides A^N by N!, and both overflow a double inside ordinary contact-centre range: 171! is Infinity, so the ratio becomes NaN from 171 agents up — roughly 1,020 contacts in a half-hour at a 300-second AHT.

This engine uses the recurrence instead:

B(0) = 1
B(n) = A * B(n-1) / (n + A * B(n-1))

Every intermediate value stays inside (0, 1], so there is no overflow at any load. At extremely low load and very high agent counts the value underflows to exactly 0, which is the correct limit and stays benign — service level then evaluates to 1, not NaN.

Probability of waiting

For N > A:

rho = A / N
C = B(N) / (1 - rho + rho * B(N))

For N <= A the queue has no steady state. The engine returns C = 1 and treats the interval as overloaded rather than applying a formula whose assumptions do not hold.

Service level

SL = 1 - C * exp(-(N - A) * (T / AHT))

An overloaded interval reports SL = 0. An idle interval — no contacts — reports service level as not applicable, not 100%. See below.

Occupancy

Occupancy = A / N

Capped at 100%. A ratio above 1 means the interval is overloaded rather than that agents are more than fully utilised, and the stability field carries that distinction, so nothing is lost by the cap.

Required productive agents

The smallest integer N satisfying all three conditions:

N > A                     the queue has a steady state
A / N <= O_max            the staffing level is physically achievable
SL(N) >= SL_target        the service target is met

The search starts at:

N = max(floor(A) + 1, ceil(A / O_max))

which satisfies the first two by construction. Occupancy falls monotonically as N grows, so every later candidate satisfies them too — incrementing can only tighten service level, and the two constraints can never be traded off against one another.

This is the part most Erlang C implementations get wrong. A search that stops at the first N meeting service level alone will, at moderate to high load, return a headcount whose occupancy is unachievable. At A = 60 Erlangs with an 80%/20s target, a service-level-only search returns 67 agents running at 89.6% occupancy; enforcing an 85% cap returns 71. At 30% shrinkage that is 96 scheduled agents versus 102 — six per interval.

The search carries the Erlang B recurrence forward one term per increment rather than rebuilding it, which keeps it linear in the answer. In practice it converges in one to a few dozen iterations. A defensive bound of 10,000 iterations returns an ITERATION_LIMIT error rather than a best-effort number.

Shrinkage

Applied last, never to the offered load, the AHT, or inside the Erlang C formula:

RequiredScheduled = ceil(RequiredProductive / (1 - S))

Estimated performance from scheduled staffing

When scheduled agents are supplied, the productive-agent assumption is:

AvailableProductive = floor(ScheduledAgents * (1 - S))

That integer feeds the Erlang C calculations. If it does not exceed offered load, the interval is reported as overloaded with a failing service level — never as NaN, infinity, or a misleadingly high percentage.

A floating-point trap worth knowing about

These two conversions must round-trip: staffing that exactly meets requirement has to yield the required productive agents back. Naively they do not, because 1 - S is not exactly representable in binary. At the default 30% shrinkage, 63 productive agents grosses up to 90 scheduled, and 90 * 0.7 evaluates to 62.999999999999993 — so the interval would report one agent short and a failing service level for staffing that is exactly right. It recurs 143 times below 5,000 agents.

The engine applies a 1e-9 tolerance to both conversions, which fixes the round trip without changing any correct answer. A property test guards it across the full parameter grid.

Staffing gap

Gap = ScheduledAgents - RequiredScheduled

Negative is understaffed, zero is exact, positive is surplus.

Edge-case rules

These are choices, not derivations. They are listed here because the answer has to be consistent and documented.

Case Rule Why
V = 0 Required staff 0; service level and occupancy not applicable (null) Reporting 100% would count an overnight interval with no contacts as a service-level success, inflating the “target met” summary metric. A null keeps it out of both numerator and denominator.
V > 0 and AHT <= 0 Input error on the AHT field A handle time of zero against real volume is a data problem, not a staffing answer.
V = 0 and AHT null Accepted There is nothing to handle, so there is no handle time to require.
Negative V, AHT, I or scheduled agents Input error  
Null, NaN or infinite input Typed error, surfaced as a data-quality message Never coerced to a default.
SL_target > 99.9% Setting rejected as out of range Service level approaches 100% but never reaches it. A 100% target is only ever “met” once C * exp(...) underflows the double, which makes the answer an artefact of precision rather than a staffing result.
Scheduled agents supplied but zero, against real volume Overloaded; service level 0  

Aggregation, and the one field that needs care

Three of the four numeric fields are straightforward:

Average handle time is the exception, and it is the most likely source of a wrong answer in the field. A naive AVERAGE(AHT) averages averages across whatever grain the visual is displaying, and the offered load comes out wrong in a way that looks entirely plausible. Use a correctly weighted measure:

AHT = DIVIDE(SUM(HandleSeconds), SUM(Contacts))

The sample dataset stores HandleSeconds rather than an average precisely so this measure is the natural thing to write.

Tolerances

Constant Value Purpose
SERVICE_LEVEL_EPSILON 1e-12 Absorbs the last bit of noise when comparing service level to target. The tightest real fixture misses by 0.44 of a point, so this can never change a meaningful answer.
OCCUPANCY_EPSILON 1e-9 Keeps an exact ratio such as 8.5 / 0.85 from rounding up to an extra agent.
SHRINKAGE_EPSILON 1e-9 Makes the two shrinkage conversions round-trip.
MAX_SERVICE_LEVEL_TARGET 0.999 The highest target with a finite answer.
MAX_SEARCH_ITERATIONS 10,000 Defensive bound; never reached in practice.

How these numbers are verified

The engine’s Erlang B recurrence is checked against an exact implementation of the same mathematics, written in BigInt rational arithmetic (test/fixtures/exactErlang.ts). The two are deliberately opposite:

  Engine Oracle
Formula Erlang B recurrence Erlang B closed form
Arithmetic IEEE-754 doubles Exact integer rationals
Overflow Impossible by construction Impossible — BigInt has no ceiling
Rounding Every operation None, except the final conversion

They share no code path, no formula and no number representation, so agreement between them is evidence rather than a tautology. At 500 Erlangs and 589 agents the oracle’s numerator runs to about 1,590 decimal digits — a calculation a double cannot express at all, and the reason the closed form had to be abandoned in the engine in the first place.

Service level adds one transcendental term. Its exponent is exactly rational, so the oracle computes it exactly and evaluates the exponential by fixed-point Taylor series to 60 decimal places — about 44 digits beyond a double.

Worst observed disagreement: 1.1e-15 relative, a few units in the last place. The test threshold is 1e-13, roughly two orders of magnitude clear of the noise floor.

The oracle is also tested for discrimination — that it rejects the factorial implementation at the loads where that overflows, accepts it below its ceiling, and rejects a recurrence with a deliberate sign error. A comparison everything passes would prove nothing.

What this does not establish

Exact arithmetic confirms the formulas were implemented correctly. It cannot confirm they are the right formulas. These remain specification decisions, documented above and unconfirmed against real-world staffing outcomes:

Confirming those needs either a published reference or an analyst who already knows what the answer should be for a handful of real intervals.

What this does not model

Version 1 is deliberately narrow. Out of scope: abandonment and Erlang A, retrials and callbacks, multi-skill or skill-based routing, queueing simulation, forecast generation, schedule or break optimisation, and hiring plans.

Erlang C assumes Poisson arrivals, exponentially distributed handle times, no abandonment, and infinite queue patience. Real queues abandon, which means Erlang C is generally conservative — it tends to recommend slightly more staff than a model with abandonment would. That is usually the safer direction for a staffing plan, but it is an assumption worth stating to anyone using the numbers.