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
Delays
Learn about delays in systems, how they create oscillations, and their impact on system behavior and decision-making.
Take the QuizWaiting 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 — 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
| Metric | Value |
|---|---|
| served | 26.00 |
| reneged | 24.00 |
| renege_fraction | 0.48 |
# 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
| Metric | Value |
|---|---|
| served | 48.00 |
| reneged | 2.00 |
| renege_fraction | 0.04 |
- 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.