Simulate a single-server service line with random arrivals and check the results against queueing theory.
Why it matters
Queueing formulas need tidy assumptions. Real service lines have lunch rushes, two kinds of customers, a server who takes breaks and a machine that breaks down. When the maths gets messy, OR analysts simulate: they build a small model of the system in code, run it thousands of times with random inputs, and measure what happens. Discrete-event simulation (DES) is used to design hospitals, factories, airports and warehouses, and to test a schedule or layout before anyone spends money on it.
The core idea
A discrete-event simulation jumps from one event to the next instead of ticking through every second. Nothing in the model changes between events.
- Entities: the things that flow (customers, patients, parts).
- Resources: what entities compete for (servers, beds, machines).
- Events: moments when the state changes (an arrival, a service start, a departure).
- State: what you track (queue length, which servers are busy).
- Clock: the current simulated time.
- Event list: future events sorted by time. The simulation repeatedly pops the earliest event, advances the clock to it, updates the state, and schedules any new events it causes.
A one-server line has two event types. An arrival joins the queue (or starts service if the server is free) and schedules the next arrival. A departure frees the server and starts the next person in line.
A one-server line in a few lines of Python
For one server, first come first served, you do not even need an explicit event list. Each customer starts service when they arrive or when the server becomes free, whichever is later:
def simulate_line(arrivals, services):
out = []
server_free_at = 0.0
for arrive, service in zip(arrivals, services):
start = max(arrive, server_free_at)
finish = start + service
out.append((start, finish, start - arrive)) # wait = start - arrival
server_free_at = finish
return out
Check it by hand. Customers arrive at times 0, 1, 2 and 5 and need 2, 2, 1 and 1 minutes:
| customer | arrives | starts | finishes | waits |
|---|---|---|---|---|
| 1 | 0 | 0 | 2 | 0 |
| 2 | 1 | 2 | 4 | 1 |
| 3 | 2 | 4 | 5 | 2 |
| 4 | 5 | 5 | 6 | 0 |
Hand-checking a tiny case like this is verification: making sure the code does what you meant.
Adding randomness
Random inputs come from probability distributions. For an M/M/1 coffee kiosk with 1 arrival per minute and 2 services per minute:
import random
rng = random.Random(42) # fixed seed: the same run every time
t = 0.0
arrivals, services = [], []
for _ in range(50000):
t += rng.expovariate(1.0) # time to the next arrival
arrivals.append(t)
services.append(rng.expovariate(2.0))
Theory says Wq = rho / (mu - lambda) = 0.5 / 1 = 0.5 minutes and half of all customers wait. A long simulated run lands close to both numbers. Comparing a simulation with a case you can solve exactly is how you build trust before you add the messy details that the formulas cannot handle: a rush hour, a second barista from 8 to 10, or customers who leave when the line is longer than six.
Always fix the seed while developing, so results are reproducible and bugs can be found. Then change the seed to get independent runs.
Running experiments properly
- Warm-up. A simulation that starts empty is not typical. Discard the first stretch of output (the warm-up period) or start from a realistic state.
- Replications. One run is one random story. Run many independent replications (different seeds) and report the average with a confidence interval, for example
mean +/- 1.96 * sd / sqrt(n). - Same random numbers for comparisons. When comparing two designs, give both the same seeds (common random numbers), so differences come from the design and not from luck.
- Validation. Compare the model's output with real data (average waits last month). Verification asks "is the code right?"; validation asks "is the model right?"
Tools
In Python, SimPy is a popular open-source DES library. Processes are generator functions, and resources handle the queueing for you:
import simpy
def customer(env, counter, service_time, waits):
arrive = env.now
with counter.request() as req:
yield req # wait for the server
waits.append(env.now - arrive)
yield env.timeout(service_time) # being served
env = simpy.Environment()
counter = simpy.Resource(env, capacity=1)
Commercial tools such as Arena, AnyLogic, FlexSim and Simio add drag-and-drop modelling and animation, which helps a lot when you present to managers.
Common mistakes
- Trusting one run. Report several replications and a confidence interval.
- No warm-up. Starting empty makes waits look shorter than they really are.
- Unverified code. Always test a tiny hand-checked case and a case with a known formula.
- Wrong rates.
expovariate(rate)takes a rate (per minute), not a mean time. - Modelling every detail on day one. Start simple, validate, then add detail.
Quick recap
- DES jumps between events: arrivals, service starts, departures.
- For one FIFO server, start = max(arrival, server free), wait = start - arrival.
- Fix seeds while developing; use warm-up, replications and confidence intervals for results.
- Verify against hand cases and formulas; validate against real data.
Try it
In the Do tab, simulate a service line and check it against the M/M/1 formula.