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