Bank Queue Simulation with SimPy: The Classic Example
The bank queue is the "Hello World" of simulation. It's simple enough to understand, complex enough to be interesting, and applicable to dozens of other scenarios.
The Basic Bank Model
import simpy
import random
def customer(env, name, tellers, stats):
"""Customer arrives, waits for teller, completes transaction."""
arrival = env.now
with tellers.request() as req:
yield req
wait = env.now - arrival
stats['waits'].append(wait)
# Transaction time
transaction = random.expovariate(1/5)
yield env.timeout(transaction)
stats['transactions'].append(transaction)
stats['departures'].append(env.now)
def arrivals(env, tellers, stats, arrival_rate):
i = 0
while True:
yield env.timeout(random.expovariate(arrival_rate))
env.process(customer(env, f"Customer {i}", tellers, stats))
i += 1
# Run simulation
env = simpy.Environment()
tellers = simpy.Resource(env, capacity=3)
stats = {'waits': [], 'transactions': [], 'departures': []}
env.process(arrivals(env, tellers, stats, arrival_rate=1/2))
env.run(until=480) # 8 hours
print(f"Customers served: {len(stats['waits'])}")
print(f"Average wait: {sum(stats['waits'])/len(stats['waits']):.2f} minutes")
Multiple Transaction Types
Different services take different times:
class Bank:
def __init__(self, env, num_tellers):
self.env = env
self.tellers = simpy.Resource(env, capacity=num_tellers)
self.stats = []
def customer(self, customer_id, transaction_type):
arrival = self.env.now
transaction_times = {
'deposit': (2, 5),
'withdrawal': (1, 3),
'loan_inquiry': (10, 20),
'account_opening': (15, 30)
}
with self.tellers.request() as req:
yield req
wait = self.env.now - arrival
min_t, max_t = transaction_times.get(transaction_type, (3, 7))
service = random.uniform(min_t, max_t)
yield self.env.timeout(service)
self.stats.append({
'customer': customer_id,
'type': transaction_type,
'wait': wait,
'service': service
})
def arrivals(env, bank):
customer_id = 0
types = ['deposit', 'withdrawal', 'loan_inquiry', 'account_opening']
probs = [0.4, 0.35, 0.15, 0.1]
while True:
yield env.timeout(random.expovariate(1/3))
transaction_type = random.choices(types, weights=probs)[0]
env.process(bank.customer(customer_id, transaction_type))
customer_id += 1
Express Lane
Separate queue for quick transactions:
class BankWithExpressLane:
def __init__(self, env, regular_tellers, express_tellers, express_threshold):
self.env = env
self.regular = simpy.Resource(env, capacity=regular_tellers)
self.express = simpy.Resource(env, capacity=express_tellers)
self.express_threshold = express_threshold # Max service time for express
def customer(self, customer_id, expected_service):
arrival = self.env.now
# Choose lane
if expected_service <= self.express_threshold:
teller = self.express
lane = "express"
else:
teller = self.regular
lane = "regular"
with teller.request() as req:
yield req
yield self.env.timeout(expected_service)
print(f"Customer {customer_id} served at {lane} lane, waited {self.env.now - arrival - expected_service:.1f}")
Balking and Reneging
Customers who leave:
def impatient_customer(env, name, tellers, max_queue, patience):
"""Customer may balk or renege."""
arrival = env.now
# Balking: refuse to join long queue
if len(tellers.queue) >= max_queue:
print(f"{name} balked at {arrival:.1f} (queue too long)")
return "balked"
with tellers.request() as req:
# Reneging: leave after waiting too long
result = yield req | env.timeout(patience)
if req in result:
# Got served
yield env.timeout(random.expovariate(1/5))
return "served"
else:
# Ran out of patience
print(f"{name} reneged at {env.now:.1f} (waited too long)")
return "reneged"
VIP Customers
Priority service for special customers:
def bank_with_vip(env, tellers):
def vip_customer(customer_id):
with tellers.request(priority=1) as req: # High priority
yield req
yield env.timeout(random.uniform(3, 8))
print(f"VIP {customer_id} served at {env.now:.1f}")
def regular_customer(customer_id):
with tellers.request(priority=5) as req: # Lower priority
yield req
yield env.timeout(random.uniform(2, 6))
print(f"Regular {customer_id} served at {env.now:.1f}")
# VIP customers jump the queue
tellers = simpy.PriorityResource(env, capacity=3)
Peak Hours Modelling
def time_varying_arrivals(env, bank):
"""Arrival rate varies by time of day."""
customer_id = 0
while True:
hour = (env.now / 60) % 24
# Peak hours
if 12 <= hour <= 14: # Lunch rush
rate = 1/1 # 1 per minute
elif 9 <= hour <= 11 or 15 <= hour <= 17:
rate = 1/2
else:
rate = 1/5
yield env.timeout(random.expovariate(rate))
env.process(bank.customer(customer_id))
customer_id += 1
Queue Display Board
Track and display queue status:
class QueueDisplay:
def __init__(self, env, tellers):
self.env = env
self.tellers = tellers
self.log = []
def monitor(self, interval=1):
while True:
self.log.append({
'time': self.env.now,
'waiting': len(self.tellers.queue),
'serving': self.tellers.count,
'available': self.tellers.capacity - self.tellers.count
})
# Estimate wait time
if self.tellers.count == self.tellers.capacity and self.tellers.queue:
estimated_wait = len(self.tellers.queue) * 5 / self.tellers.capacity
print(f"[{self.env.now:.0f}] Queue: {len(self.tellers.queue)}, Est. wait: {estimated_wait:.0f} min")
yield self.env.timeout(interval)
Complete Bank Simulation
import simpy
import random
import numpy as np
import pandas as pd
class BankSimulation:
def __init__(self, env, config):
self.env = env
self.config = config
self.tellers = simpy.Resource(env, capacity=config['tellers'])
self.customer_records = []
def customer(self, customer_id, transaction_type):
arrival = self.env.now
record = {
'id': customer_id,
'type': transaction_type,
'arrival': arrival
}
# Balking check
if len(self.tellers.queue) > self.config['max_queue']:
record['outcome'] = 'balked'
self.customer_records.append(record)
return
patience = random.expovariate(1/self.config['mean_patience'])
with self.tellers.request() as req:
result = yield req | self.env.timeout(patience)
if req in result:
record['wait'] = self.env.now - arrival
# Service time by type
service_params = self.config['service_times'][transaction_type]
service = random.uniform(*service_params)
yield self.env.timeout(service)
record['service'] = service
record['outcome'] = 'served'
record['departure'] = self.env.now
else:
record['wait'] = patience
record['outcome'] = 'reneged'
self.customer_records.append(record)
def arrivals(self):
customer_id = 0
types = list(self.config['service_times'].keys())
probs = self.config['type_probabilities']
while True:
# Time-varying rate
hour = (self.env.now / 60) % 24
rate = self.get_arrival_rate(hour)
yield self.env.timeout(random.expovariate(rate))
transaction_type = random.choices(types, weights=probs)[0]
self.env.process(self.customer(customer_id, transaction_type))
customer_id += 1
def get_arrival_rate(self, hour):
if 12 <= hour <= 14:
return self.config['peak_rate']
elif 9 <= hour <= 17:
return self.config['normal_rate']
else:
return self.config['off_peak_rate']
def monitor(self, interval=5):
while True:
yield self.env.timeout(interval)
def run(self, duration):
self.env.process(self.arrivals())
self.env.process(self.monitor())
self.env.run(until=duration)
def report(self):
df = pd.DataFrame(self.customer_records)
served = df[df['outcome'] == 'served']
balked = df[df['outcome'] == 'balked']
reneged = df[df['outcome'] == 'reneged']
print("\n=== Bank Simulation Report ===")
print(f"Total arrivals: {len(df)}")
print(f"Served: {len(served)} ({len(served)/len(df):.1%})")
print(f"Balked: {len(balked)} ({len(balked)/len(df):.1%})")
print(f"Reneged: {len(reneged)} ({len(reneged)/len(df):.1%})")
if len(served) > 0:
print(f"\nWait time (served customers):")
print(f" Mean: {served['wait'].mean():.2f} min")
print(f" Max: {served['wait'].max():.2f} min")
print(f" 90th pct: {served['wait'].quantile(0.9):.2f} min")
print(f"\nBy transaction type:")
print(served.groupby('type')['wait'].mean())
return df
# Configuration
config = {
'tellers': 4,
'max_queue': 10,
'mean_patience': 8,
'peak_rate': 1.0,
'normal_rate': 0.5,
'off_peak_rate': 0.2,
'service_times': {
'deposit': (2, 4),
'withdrawal': (1, 3),
'inquiry': (3, 8),
'complex': (10, 20)
},
'type_probabilities': [0.35, 0.35, 0.2, 0.1]
}
# Run
random.seed(42)
env = simpy.Environment()
bank = BankSimulation(env, config)
bank.run(duration=480) # 8 hours
results = bank.report()
Summary
The bank queue teaches: - Basic queue dynamics - Multiple service types - Customer behaviour (balking, reneging) - Priority handling - Time-varying demand
Master the bank. Model anything.
Next Steps
Build Professional Simulations
Break free from commercial software and learn how to build powerful, industry-standard simulations in Python. The Complete Simulation in Python with SimPy Bootcamp gives you everything you need.
Explore the Bootcamp