Modelling Servers in SimPy: From Single to Multiple
Servers are the workhorses of simulation. Tills, machines, staff, computers—anything that processes entities and takes time.
The Single Server
One resource, one capacity:
import simpy
import random
def customer(env, name, server):
arrival = env.now
print(f"{name} arrives at {arrival:.2f}")
with server.request() as req:
yield req
wait = env.now - arrival
print(f"{name} starts service at {env.now:.2f} (waited {wait:.2f})")
service_time = random.expovariate(1/5)
yield env.timeout(service_time)
print(f"{name} leaves at {env.now:.2f}")
env = simpy.Environment()
server = simpy.Resource(env, capacity=1)
Classic M/M/1 queue territory.
Multiple Parallel Servers
Increase capacity:
# 3 identical parallel servers
servers = simpy.Resource(env, capacity=3)
Now up to 3 customers can be served simultaneously. Think: multiple tills at a supermarket.
Multiple Server Types
Different servers for different tasks:
class ServicePoint:
def __init__(self, env, name, capacity, service_rate):
self.name = name
self.resource = simpy.Resource(env, capacity=capacity)
self.service_rate = service_rate
def serve(self, env, customer_name):
with self.resource.request() as req:
yield req
service_time = random.expovariate(self.service_rate)
yield env.timeout(service_time)
# Different service points
check_in = ServicePoint(env, "Check-in", capacity=4, service_rate=1/3)
security = ServicePoint(env, "Security", capacity=2, service_rate=1/5)
boarding = ServicePoint(env, "Boarding", capacity=1, service_rate=1/2)
def passenger(env, name):
yield from check_in.serve(env, name)
yield from security.serve(env, name)
yield from boarding.serve(env, name)
Server with Setup Time
Some servers need setup between jobs:
def server_with_setup(env, name, job_queue, setup_time, process_time):
last_job_type = None
while True:
job = yield job_queue.get()
# Setup if job type changed
if job['type'] != last_job_type:
print(f"{name} setting up for {job['type']}")
yield env.timeout(setup_time)
last_job_type = job['type']
# Process
yield env.timeout(process_time)
print(f"{name} completed job {job['id']}")
Server with Breaks
Staff take breaks:
def server_with_breaks(env, resource, work_duration, break_duration):
while True:
# Work period
yield env.timeout(work_duration)
# Take break (temporarily reduce capacity)
print(f"Server taking break at {env.now}")
resource.capacity -= 1
yield env.timeout(break_duration)
resource.capacity += 1
print(f"Server back at {env.now}")
Better approach—use preemptive resource:
def worker(env, tasks, priority=10):
while True:
with tasks.request(priority=priority) as req:
yield req
yield env.timeout(random.expovariate(1/5))
def break_controller(env, worker_proc, work_time, break_time):
while True:
yield env.timeout(work_time)
worker_proc.interrupt("break")
yield env.timeout(break_time)
Server Pool with Load Balancing
Route to least busy server:
class ServerPool:
def __init__(self, env, num_servers, service_rate):
self.env = env
self.servers = [
simpy.Resource(env, capacity=1)
for _ in range(num_servers)
]
self.service_rate = service_rate
def get_least_busy(self):
"""Return server with shortest queue."""
return min(self.servers, key=lambda s: len(s.queue))
def serve(self, customer_name):
server = self.get_least_busy()
with server.request() as req:
yield req
yield self.env.timeout(random.expovariate(self.service_rate))
pool = ServerPool(env, num_servers=3, service_rate=1/5)
def customer(env, name, pool):
yield from pool.serve(name)
Server Utilisation Tracking
class TrackedServer:
def __init__(self, env, capacity):
self.env = env
self.resource = simpy.Resource(env, capacity=capacity)
self.busy_time = 0
self.last_change = 0
self.last_count = 0
def request(self):
return TrackedRequest(self)
def update_stats(self):
elapsed = self.env.now - self.last_change
self.busy_time += self.last_count * elapsed
self.last_change = self.env.now
self.last_count = self.resource.count
@property
def utilisation(self):
self.update_stats()
if self.env.now == 0:
return 0
return self.busy_time / (self.env.now * self.resource.capacity)
class TrackedRequest:
def __init__(self, tracked_server):
self.server = tracked_server
self.request = None
def __enter__(self):
self.request = self.server.resource.request()
return self.request.__enter__()
def __exit__(self, *args):
self.server.update_stats()
return self.request.__exit__(*args)
Server States
Model server states explicitly:
from enum import Enum
class ServerState(Enum):
IDLE = "idle"
BUSY = "busy"
BREAKDOWN = "breakdown"
MAINTENANCE = "maintenance"
class StatefulServer:
def __init__(self, env, name):
self.env = env
self.name = name
self.state = ServerState.IDLE
self.resource = simpy.Resource(env, capacity=1)
def process_job(self, job):
self.state = ServerState.BUSY
with self.resource.request() as req:
yield req
yield self.env.timeout(job['duration'])
self.state = ServerState.IDLE
def breakdown(self, repair_time):
self.state = ServerState.BREAKDOWN
yield self.env.timeout(repair_time)
self.state = ServerState.IDLE
Speed-Dependent Servers
Faster service for higher utilisation:
def adaptive_server(env, queue, base_rate, speedup_threshold=5):
while True:
job = yield queue.get()
# Speed up if queue is long
if len(queue.items) > speedup_threshold:
service_time = random.expovariate(base_rate * 1.5) # 50% faster
else:
service_time = random.expovariate(base_rate)
yield env.timeout(service_time)
Batch Servers
Process multiple items at once:
def batch_server(env, queue, batch_size, process_time):
while True:
batch = []
# Collect batch
while len(batch) < batch_size:
item = yield queue.get()
batch.append(item)
# Process entire batch at once
yield env.timeout(process_time)
print(f"Processed batch of {len(batch)} at {env.now}")
Complete Example: Multi-Server System
import simpy
import random
class Server:
def __init__(self, env, name, service_rate):
self.env = env
self.name = name
self.service_rate = service_rate
self.resource = simpy.Resource(env, capacity=1)
self.customers_served = 0
self.total_service_time = 0
def serve(self, customer_id):
with self.resource.request() as req:
yield req
service_time = random.expovariate(self.service_rate)
yield self.env.timeout(service_time)
self.customers_served += 1
self.total_service_time += service_time
return service_time
@property
def utilisation(self):
if self.env.now == 0:
return 0
return self.total_service_time / self.env.now
def customer(env, customer_id, servers):
arrival = env.now
# Choose shortest queue
server = min(servers, key=lambda s: len(s.resource.queue))
service_time = yield from server.serve(customer_id)
return {
'id': customer_id,
'arrival': arrival,
'departure': env.now,
'server': server.name,
'wait': env.now - arrival - service_time,
'service': service_time
}
def arrivals(env, servers, results, arrival_rate):
i = 0
while True:
yield env.timeout(random.expovariate(arrival_rate))
result = env.process(customer(env, i, servers))
i += 1
# Run simulation
random.seed(42)
env = simpy.Environment()
servers = [Server(env, f"Server{i}", 1/4) for i in range(3)]
results = []
env.process(arrivals(env, servers, results, arrival_rate=1/2))
env.run(until=1000)
# Report
for s in servers:
print(f"{s.name}: served {s.customers_served}, utilisation {s.utilisation:.1%}")
Summary
Server modelling:
- Single server: Resource(env, capacity=1)
- Multiple parallel: Resource(env, capacity=n)
- Different types: separate Resource instances
- Track utilisation, states, and performance
The server is the atom of simulation. Master it.
Next Steps
Discover the Power of Simulation
Want to become a go-to expert in simulation with Python? The Complete Simulation Bootcamp will show you how simulation can transform your career and your projects.
Explore the Bootcamp