Buzen's Algorithm
Queueing theory studies systems where jobs arrive, wait, get served, and depart. A queueing network connects multiple queues - jobs flow from one to the next, like packets through routers or requests through microservices. Analyzing these networks means computing steady-state probabilities: in the long run, what fraction of time does the system spend in each configuration?
Fifty jobs circulating among ten servers can occupy over 12 billion distinct configurations. Yet Buzen’s 1973 algorithm computes the exact probability of every state with only a few hundred operations - a reduction from to . This post derives the algorithm from first principles and implements it.
Queueing Basics
A single queue has jobs arriving at rate (lambda) and a server processing them at rate (mu). The ratio is the utilization - the fraction of time the server is busy. For stability, ; otherwise jobs accumulate faster than they’re served.
The simplest model is the M/M/1 queue: Poisson arrivals (memoryless, hence “M”), exponential service times (also “M”), and one server. In steady state, the probability of having exactly jobs in the system is:
A geometric distribution - most likely to be empty, exponentially less likely to have more jobs.
Queueing Networks
Real systems chain queues together. A job might visit a CPU queue, then a disk queue, then return to the CPU. The routing matrix specifies transition probabilities: is the probability a job leaving queue goes to queue .
In open networks, jobs arrive from outside and eventually depart. Jackson (1963) proved that under Poisson arrivals and exponential service, the steady-state distribution has product form - the joint probability factors into independent terms for each queue.
In closed networks, a fixed population of jobs circulates forever. No arrivals, no departures - just jobs moving between queues. Gordon and Newell (1967) extended Jackson’s result: closed networks also have product-form solutions, but with a normalization constant that’s expensive to compute.
Notation
| Symbol | Meaning |
|---|---|
| Total number of jobs in the closed network | |
| Number of queues (service centers) | |
| Service rate at queue | |
| Visit ratio - relative frequency of visits to queue | |
| Relative load at queue (service demand per visit) | |
| Number of jobs at queue in a given state | |
| Normalization constant for jobs |
The visit ratios come from solving the traffic equations . They represent how often each queue is visited relative to some reference queue.
The Normalization Problem
For a closed network with jobs and queues, the steady-state probability of configuration where is:
The product captures the relative likelihood of each configuration - states that concentrate jobs at queues with large contribute more to the un-normalized sum. The normalization constant ensures probabilities sum to 1:
where is the state space - all ways to distribute jobs among queues:
For and , the state space contains configurations. Computing by summing over all of them is infeasible.
The Naive Approach
Direct enumeration iterates over every state in , computes , and accumulates the sum. The state space size is the number of ways to place indistinguishable jobs into queues - a stars-and-bars combinatorial problem:
| Network | States | Time |
|---|---|---|
| , | 1001 | |
| , | 316251 | |
| , | ||
| , |
A network of 10 queues and 50 jobs requires iterating over tens of billions of states. Even modern hardware chokes on this.
The structure of the sum suggests redundancy. Each term shares factors with many others. The sum is essentially an -fold convolution - a hint that dynamic programming might help.
Recursive Decomposition
Buzen recognized that can be computed recursively by adding one queue at a time.
Define as the normalization constant for jobs distributed among the first queues:
By definition, .
Now partition the states based on queue ‘s occupancy. Either queue has zero jobs, or it has at least one:
-
Zero at queue : Queue contributes . The remaining jobs are distributed among queues through . This contributes .
-
At least one at queue : Factor out for one job at queue . The remaining jobs (including any additional ones at queue ) distribute among all queues. This contributes .
The recurrence:
Base cases:
- for all (one way to place zero jobs - all queues empty)
- for (no queues means no way to place jobs)
This recurrence computes using only operations instead of enumerating all states - a count that grows as for fixed .
Why Dynamic Programming is Optimal
The recurrence has optimal substructure: depends only on and . Without memoization, we’d recompute values exponentially.
An alternative formulation suggests itself. The sum is the -th coefficient of the generating function:
Using FFT-based polynomial multiplication, we could compute this product in time. But Buzen’s recurrence achieves by exploiting the special structure - each factor is a geometric series, so the convolution simplifies to a single pass.
The bound is tight for this recurrence. We must fill an table, each entry requiring work.
Implementation
The algorithm maintains a single array representing the current “column” . Each iteration incorporates one more queue:
def buzen(X: list[float], N: int) -> list[float]:
"""Compute normalization constants G(0), G(1), ..., G(N)"""
g = [0.0] * (N + 1)
g[0] = 1.0
for x in X:
for n in range(1, N + 1):
g[n] += x * g[n - 1]
return g
That’s it. After processing all queues, g[n] contains for each from to .
Walkthrough
For queues with , , computing :
Initialize:
Process queue 1 ():
After: which matches .
Process queue 2 ():
Final:
Verify directly:
Computing Performance Metrics
The real payoff comes from using values to extract performance metrics without revisiting the state space.
Marginal Queue Length Distribution
The probability that queue has at least jobs:
Intuition: fix jobs at queue (contributing ), distribute the remaining arbitrarily (summing to ), normalize by .
def prob_at_least(g: list[float], X: list[float], i: int, k: int) -> float:
"""P(queue i has >= k jobs)"""
N = len(g) - 1
if k > N:
return 0.0
return (X[i] ** k) * g[N - k] / g[N]
Expected Queue Length
The expected number of jobs at queue :
def expected_queue_length(g: list[float], X: list[float], i: int) -> float:
"""E[jobs at queue i]"""
N = len(g) - 1
total = 0.0
x_power = X[i]
for k in range(1, N + 1):
total += x_power * g[N - k]
x_power *= X[i]
return total / g[N]
Throughput
The throughput (completion rate) at queue follows from the arrival theorem:
where is the visit ratio. The ratio acts as a system-wide throughput factor.
def throughput(g: list[float], e: list[float]) -> list[float]:
"""Throughput at each queue"""
N = len(g) - 1
ratio = g[N - 1] / g[N]
return [e_i * ratio for e_i in e]
All these metrics come essentially free once we have the array - no state enumeration required.
Numerical Considerations
For large or extreme values, can overflow or underflow floating-point. Two approaches:
Scaling: Periodically divide all entries by during computation. This keeps values bounded but requires care when computing ratios.
Log-domain: Work with instead. The recurrence becomes:
Use the log-sum-exp trick for numerical stability. This adds overhead but handles extreme cases.
For most practical networks (moderate , reasonable utilizations), standard double precision suffices.
Complexity Comparison
| Approach | Time | Space |
|---|---|---|
| Naive enumeration | ||
| Buzen’s algorithm | ||
| FFT convolution |
For , : naive requires operations, Buzen requires .
The algorithm also produces all intermediate values as a byproduct - exactly what we need for performance metrics.
Historical Note
Buzen developed this algorithm during his PhD at Harvard (1971) while studying computer system performance. At the time, Gordon-Newell networks were known to have elegant theoretical properties, but practitioners couldn’t compute anything with them - the state spaces were too large.
Buzen’s convolution algorithm made closed queueing network analysis practical. It became a foundation of computer performance modeling throughout the 1970s and 1980s, enabling capacity planning for mainframes and early distributed systems.
The same year (1973), Buzen founded BGS Systems, one of the first companies focused on computer performance analysis software - built on these algorithms.
Conclusion
Buzen’s algorithm reduces a sum over billions of states to a few hundred multiply-adds. The trick is recognizing that the normalization constant decomposes into sequential convolutions - add one queue at a time, and the combinatorial explosion collapses into a simple recurrence.
The algorithm exemplifies a broader pattern: when a massive sum has product structure, dynamic programming often finds a shortcut. Here, the product-form solution of Gordon-Newell networks provides exactly that structure. What looks like an intractable enumeration problem becomes arithmetic.
For practitioners in the 1970s, this was transformative. Closed queueing networks had elegant theory but were computationally useless until Buzen’s algorithm made them practical. The same ideas - convolution, dynamic programming, exploiting algebraic structure - appear throughout performance modeling, from Mean Value Analysis to modern cloud capacity planning.
Further Reading
- Buzen’s algorithm - derivation and pseudocode
- Jackson network - open network theory
- Gordon-Newell theorem - closed network product form
References
- Buzen, J. P. (1973). Computational algorithms for closed queueing networks with exponential servers. Communications of the ACM, 16(9), 527-531.
- Gordon, W. J., & Newell, G. F. (1967). Closed queuing systems with exponential servers. Operations Research, 15(2), 254-265.
- Jackson, J. R. (1963). Jobshop-like queueing systems. Management Science, 10(1), 131-142.
- Reiser, M., & Lavenberg, S. (1980). Mean value analysis of closed multichain queueing networks. Journal of the ACM, 27(2), 313-322.