Learn · updated 2026-09-23

SimPy Examples: Queues in Python

SimPy is a process-based discrete-event simulation library for Python. These four short programs build the same queues as the interactive models on this site, and each prints its result next to the value queueing theory predicts. All output below comes from running the code with SimPy 4.1.2 on Python 3.12.

How SimPy thinks

A SimPy model is a set of processes — ordinary Python generator functions — running inside an Environment that owns the simulation clock. A process pauses by yielding an event: env.timeout(d) resumes it after d units of simulated time, resource.request() resumes it when a server is free. The environment keeps the pending events in a priority queue ordered by time and always resumes the earliest, which is exactly the next-event loop that the discrete-event simulation page performs by hand.

Install with pip install simpy. The examples use Python’s random.Random with a fixed seed so they are reproducible.

1. Processes and the clock

clock.py
import simpy


def clock(env, name, tick):
    while True:
        print(f"{env.now:4.1f}  {name}")
        yield env.timeout(tick)


env = simpy.Environment()
env.process(clock(env, "fast", 0.5))
env.process(clock(env, "slow", 1.0))
env.run(until=2)
Output
 0.0  fast
 0.0  slow
 0.5  fast
 1.0  slow
 1.0  fast
 1.5  fast

Nothing happens between events and no time is spent simulating idle intervals. Events scheduled for the same instant run in the order they were scheduled.

2. M/M/1 queue

One server, Poisson arrivals at rate λ = 0.8 per minute, exponential service at rate μ = 1. Theory predicts a mean wait in queue of Wq = ρ/(μ − λ) = 4 minutes, with ρ = λ/μ.

mm1.py
import random
import simpy

LAMBDA, MU = 0.8, 1.0          # arrivals and services per minute
rng = random.Random(42)
waits = []


def customer(env, server):
    arrived = env.now
    with server.request() as req:
        yield req                                    # wait for the server
        waits.append(env.now - arrived)
        yield env.timeout(rng.expovariate(MU))       # service


def source(env, server):
    while True:
        yield env.timeout(rng.expovariate(LAMBDA))   # next arrival
        env.process(customer(env, server))


env = simpy.Environment()
server = simpy.Resource(env, capacity=1)
env.process(source(env, server))
env.run(until=200_000)

rho = LAMBDA / MU
print(f"customers served   {len(waits)}")
print(f"mean wait in queue {sum(waits) / len(waits):.3f}  (theory {rho / (MU - LAMBDA):.3f})")
Output
customers served   159627
mean wait in queue 3.971  (theory 4.000)

The with server.request() as req block releases the server automatically when the service timeout ends. Even 200,000 minutes of simulated time leave an error of about 1% at ρ = 0.8: queue lengths are strongly correlated from one customer to the next, so the effective number of independent observations is far below 160,000 (see Monte Carlo error). The interactive M/M/1 queue shows the same model with the queue drawn over time.

3. M/M/c queue and Erlang C

Three servers share one line; λ = 2.4 and μ = 1, so each server is busy 80% of the time. The Erlang C formula gives the probability that an arriving customer must wait.

mmc.py
import math
import random
import simpy

LAMBDA, MU, C = 2.4, 1.0, 3
rng = random.Random(7)
waits = []


def customer(env, servers):
    arrived = env.now
    with servers.request() as req:
        yield req
        waits.append(env.now - arrived)
        yield env.timeout(rng.expovariate(MU))


def source(env, servers):
    while True:
        yield env.timeout(rng.expovariate(LAMBDA))
        env.process(customer(env, servers))


def erlang_c(c, a):
    """Probability that an arrival has to wait in an M/M/c queue (a = lambda/mu)."""
    top = a ** c / math.factorial(c) * c / (c - a)
    return top / (sum(a ** k / math.factorial(k) for k in range(c)) + top)


env = simpy.Environment()
servers = simpy.Resource(env, capacity=C)
env.process(source(env, servers))
env.run(until=200_000)

a = LAMBDA / MU
p_wait = erlang_c(C, a)
print(f"P(wait)            {sum(w > 0 for w in waits) / len(waits):.3f}  (Erlang C {p_wait:.3f})")
print(f"mean wait in queue {sum(waits) / len(waits):.3f}  (theory {p_wait / (C * MU - LAMBDA):.3f})")
Output
P(wait)            0.648  (Erlang C 0.647)
mean wait in queue 1.085  (theory 1.079)

Splitting the same customers into three separate single-server lines — each one exactly the M/M/1 queue above — gives a mean wait of 4 minutes at the same 80% utilization. Pooling them into one line served by three servers cuts it to about 1.08 minutes, roughly a quarter. The M/M/c queue page explores that pooling effect interactively. The direct factorial formula is fine for small c; for hundreds of servers, compute Erlang C through the Erlang B recursion instead to avoid overflow.

4. Two stations in series

Customers order at a counter (μ₁ = 1) and then wait for a barista (μ₂ = 1.25). By Jackson’s theorem each station of this open network behaves like an independent M/M/1 queue with the same arrival rate, so the mean time in the system is 1/(μ₁ − λ) + 1/(μ₂ − λ).

tandem.py
import random
import simpy

LAMBDA, MU1, MU2 = 0.8, 1.0, 1.25
rng = random.Random(3)
times = []


def customer(env, counter, barista):
    arrived = env.now
    with counter.request() as req:
        yield req
        yield env.timeout(rng.expovariate(MU1))
    with barista.request() as req:
        yield req
        yield env.timeout(rng.expovariate(MU2))
    times.append(env.now - arrived)


def source(env, counter, barista):
    while True:
        yield env.timeout(rng.expovariate(LAMBDA))
        env.process(customer(env, counter, barista))


env = simpy.Environment()
counter = simpy.Resource(env, capacity=1)
barista = simpy.Resource(env, capacity=1)
env.process(source(env, counter, barista))
env.run(until=200_000)

theory = 1 / (MU1 - LAMBDA) + 1 / (MU2 - LAMBDA)   # Jackson: two independent M/M/1 stations
print(f"mean time in system {sum(times) / len(times):.3f}  (theory {theory:.3f})")
Output
mean time in system 7.295  (theory 7.222)

Writing the second station is one more with … request() block: the process-interaction style reads like a description of what a customer does. The event-scheduling version of the same model — with the future event list made visible — is the discrete-event simulation page.

Things these examples skip

  • Warm-up. Each run starts empty and idle, which biases early statistics low. Discard an initial period, or run long enough that it does not matter (these runs do).
  • Replications. One long run gives one estimate. Run several seeds, or use batch means, to put a confidence interval on it.
  • Customers still in the system at the end are not counted in the averages; with 160,000 completed customers their effect is negligible.

The OpenSim.zip model packages do not depend on SimPy: their simulation.py files implement the event loop directly with the standard library, so the Python and JavaScript versions can share one random-number stream.

References

  1. SimPy documentation (version 4). simpy.readthedocs.io
  2. Jackson, J. R. (1957). Networks of waiting lines. Operations Research, 5(4), 518–521. doi.org/10.1287/opre.5.4.518
  3. Kleinrock, L. (1975). Queueing Systems, Volume 1: Theory. Wiley.
  4. Banks, J., Carson, J. S., Nelson, B. L. & Nicol, D. M. (2010). Discrete-Event System Simulation, 5th ed. Pearson.