Bank Renege Simulation

At a teller running near capacity, patience decides everything: impatient customers give up before a line even forms - half walk out - while patient ones build a long queue yet nearly all get served.

Level: Intermediate

queueservicediscrete-eventreneging

  • Stocks: queue_length
  • Flows: served, reneged
  • Probes: wait_time, served, reneged, queue_length

Delays

Learn about delays in systems, how they create oscillations, and their impact on system behavior and decision-making.

Take the Quiz
simulation.py

Waiting in line at the bank

Adapted from https://simpy.readthedocs.io/en/latest/examples/bank_renege.html

A single teller runs at ~90% utilisation, so a line forms. Whether you get served comes down to one thing: your patience versus the wait. Impatient customers give up in droves; patient ones wait it out.

The knob is patience. Same teller, same congestion — flip patience and the renege rate swings from about half the customers to almost none.


import random
from tys import probe, progress

Bank with impatient customers.

def simulate(cfg: dict):

    import simpy
    env = simpy.Environment()
    num_customers = cfg["num_customers"]
    arrival_interval = cfg["arrival_interval"]
    time_in_bank = cfg["time_in_bank"]
    min_patience = cfg["min_patience"]
    max_patience = cfg["max_patience"]
    random.seed(cfg.get("seed", 42))

    counter = simpy.Resource(env, capacity=1)

    served = 0
    reneged = 0
    done = env.event()

Handle one customer's visit to the bank.

    def customer(name: str):
        nonlocal served, reneged
        arrive = env.now
        with counter.request() as req:
            patience = random.uniform(min_patience, max_patience)
            results = yield req | env.timeout(patience)
            wait = env.now - arrive
            probe("wait_time", env.now, wait)
            if req in results:
                service_time = random.expovariate(1.0 / time_in_bank)
                yield env.timeout(service_time)
                served += 1
                probe("served", env.now, served)
            else:
                reneged += 1
                probe("reneged", env.now, reneged)
        processed = served + reneged
        progress(100 * processed / num_customers)
        if processed >= num_customers:
            done.succeed({
                "served": served,
                "reneged": reneged,
                "renege_fraction": reneged / num_customers,
            })

Generate arriving customers.

    def source():
        for i in range(num_customers):
            env.process(customer(f"Customer{i:02d}"))
            yield env.timeout(random.expovariate(1.0 / arrival_interval))

Sample the line length so the queue itself is visible, not just outcomes.

    def monitor():
        while True:
            probe("queue_length", env.now, len(counter.queue))
            yield env.timeout(5.0)

    env.process(source())
    env.process(monitor())
    env.run(until=done)
    return done.value


def requirements():
    return {
        "external": ["simpy==4.1.1"],
    }
Impatient.yaml
# impatient.yaml — the default: a busy teller (rho = 12/13.3 ~ 0.9) and
# customers who will only wait 1-3 minutes, so almost half give up.
seed: 42
num_customers: 50
arrival_interval: 13.3
time_in_bank: 12.0
min_patience: 1
max_patience: 3
Charts (Impatient)

queue_length

Samples128 @ 0.00–635.00
Valuesmin 0.00, mean 0.09, median 0.00, max 1.00, σ 0.28

wait_time

Samples50 @ 0.00–633.89
Valuesmin 0.00, mean 0.94, median 0.99, max 2.94, σ 0.96

served

Samples26 @ 3.86–638.59
Valuesmin 1.00, mean 13.50, median 13.50, max 26.00, σ 7.50

reneged

Samples24 @ 18.10–560.92
Valuesmin 1.00, mean 12.50, median 12.50, max 24.00, σ 6.92
Final Results (Impatient)
MetricValue
served26.00
reneged24.00
renege_fraction0.48
Patient.yaml
# patient.yaml — the contrast: identical teller and congestion, but customers
# will wait 80-160 minutes, so nearly everyone is served.
seed: 42
num_customers: 50
arrival_interval: 13.3
time_in_bank: 12.0
min_patience: 80
max_patience: 160
Charts (Patient)

queue_length

Samples147 @ 0.00–730.00
Valuesmin 0.00, mean 3.02, median 2.00, max 10.00, σ 3.11

wait_time

Samples50 @ 0.00–719.25
Valuesmin 0.00, mean 44.45, median 41.57, max 111.37, σ 38.30

served

Samples48 @ 3.86–734.96
Valuesmin 1.00, mean 24.50, median 24.50, max 48.00, σ 13.85

reneged

Samples2 @ 523.43–564.03
Valuesmin 1.00, mean 1.50, median 1.50, max 2.00, σ 0.50
Final Results (Patient)
MetricValue
served48.00
reneged2.00
renege_fraction0.04
FAQ
Why does almost nobody wait in the impatient run, yet half leave?
With only 1-3 minutes of patience and a teller that needs about 12 minutes per customer, anyone not served immediately gives up before a line forms - so queue_length stays near zero while the renege count climbs.
Why is the patient run's line so much longer if more people are served?
Patient customers wait instead of leaving, so a real queue builds (up to ~10 waiting). The teller works through it and serves 48 of 50 - impatience, not the teller, was the bottleneck.
What sets the wait?
The teller runs at about 90% utilisation (rho = time_in_bank / arrival_interval is roughly 0.9), so a queue forms; how long each customer tolerates it is the patience range.
What is the real-world analog?
Call-center abandonment and online checkout drop-off: the same load produces very different completion rates depending on how long people are willing to wait.