Where you are. Four lessons into the rails module, you hold a working RTGS. Lesson 1 gave you the workbench and the module’s seeded payment set, lesson 2 split clearing from settlement, lesson 3 built settle_rtgs() - every payment settled one by one, at once, in reserves - and lesson 4 priced that immediacy: each payment needs its full value in reserves at the moment it settles, so the bill is set by timing and peaks, not daily totals. What lesson 4 left open is the obvious next question: what happens when a bank declines to fund the peak? This lesson answers it by measurement - a queue, ten reserve levels, one curve - and the curve turns out to be the chart the rest of the module is drawn on.
Ten guesses before dawn
It is 06:55 and Alder’s treasurer owes the desk one number: how much to fund the settlement account before the rail opens. The day ahead holds about fifty outgoing payments, sized anywhere from 10 to 200, arriving at minutes nobody knows in advance. Park 2,500 and every one of them settles the second it arrives - and most of that money sits untouched from open to close. Park 250, barely two mid-sized payments’ worth, and the account runs dry by breakfast, with payments waiting in line behind it. Neither answer is wrong. They are prices paid in different currencies: one in reserves parked, the other in delay accepted. Lesson 4 measured the first currency. This lesson buys the same day at ten different prices and prints the exchange rate.
The idea in one paragraph
A bank that cannot cover a payment right now does not have to bounce it; it can queue it, because settlement itself keeps delivering reserves - every payment the bank receives is settlement liquidity it can immediately spend. So a day’s payments can settle with far less liquidity than lesson 4’s peaks suggest, at the price of waiting for the recycling to come around. Hold the payment set fixed, sweep opening reserves across ten levels, measure the mean wait at each, and the runs trace a curve: steep where reserves are starved, flat where they are ample. That curve is the liquidity-delay frontier, and every rail this module builds is a point on it, chosen - not suffered - by the rail’s designers. RTGS parks liquidity to make delay vanish; lesson 7’s batch rail accepts hours of delay to make liquidity almost vanish; and nothing anywhere on the curve gets both for free.
A queue that refuses to go negative
Lesson 3’s settle_rtgs() had one response to a payment its payer bank could not cover: reject it. That was the honest first cut, but it punishes a timing problem as if it were a solvency problem - the bank has the money coming, just not yet. World.settle() alone would err the other way and cheerfully write the bank’s reserves negative, because miniledger v1 enforces balanced legs and matching tiers, not a floor at zero; the refusal you built in module 1 guards the customer’s deposit, not the bank’s reserve line. A real heavy-duty rail does neither. It holds the payment until the money is there.
QueuedRtgs, in this lesson’s exercise, is that hold - a wrapper around World, wrapping and never editing v1, per the module 1 rule. submit appends to a queue; drain walks the queue and settles, through the ordinary miniledger submit and settle, every payment whose paying bank currently covers it. Two design choices in drain do the real work:
- First in, first out per paying bank, not per system. A bank’s own payments leave in arrival order, but one bank’s stuck payment does not hold up another bank’s ready one. A single all-banks queue head would jam the entire rail on the first short bank.
- Rescan after every success. Each settlement delivers reserves to some payee bank, and that delivery may cover a payment queued earlier in the line. So
drainrestarts from the top after every settlement and stops only on a full pass with no progress: reserves recycle, and the loop chases the recycling until it runs out.
Even this per-bank design has a failure day: a circle of banks, each waiting to be paid before it can pay, where rescanning never finds progress. Lesson 6 constructs that day on purpose, and the cure it teaches settles the whole circle at once. Today the reserves run thin but never that thin: the queue always drains eventually, and “eventually” is exactly the thing to measure.
Ten runs of the same day
The payment set never changes: the module’s seeded day, 300 payments among the six banks across 1,440 minutes, amounts from 10 to 200. Deposits are funded by lending - loans create deposits, module 1 lesson 9 - so customers can always afford their payments; the only knob is the endowed opening reserves per bank, swept from 250 to 2,500 in steps of 250. One statistic per run: the mean minutes a payment waited between arrival and settlement, counted over every payment. A payment still stuck after the close-of-day sweep is charged its full wait to minute 1,440.
Here is the sweep, excerpted; your run prints all ten levels. The numbers are stylised - a seeded synthetic day, not a measured payment system - but the shape is the finding:
| reserves/bank | posted | settled | stuck | mean delay (min) |
|---|---|---|---|---|
| 250 | 1500 | 266 | 34 | 77.6 |
| 500 | 3000 | 279 | 21 | 28.6 |
| 1000 | 6000 | 291 | 9 | 3.2 |
| 2000 | 12000 | 299 | 1 | 0.2 |
| 2250 | 13500 | 300 | 0 | 0.0 |
| 2500 | 15000 | 300 | 0 | 0.0 |
n = 300 trials · one seeded day, six banks, opening reserves swept per bank
The posted column is the system total, reserves per bank times six banks. Read the first and last rows against each other. At 250 per bank the whole system posts 1,500 - and settles 266 payments with it, because the recycling loop spends the same reserves over and over - but the mean payment waits 77.6 minutes and 34 payments never settle at all. At 2,250 per bank nothing waits, ever, and the level above it changes nothing. In between lies the whole story: every extra 250 of reserves buys some delay back, and each buys less than the one before.
Plotted, the ten rows become the chart the module keeps returning to:
Every rail is a point on this curve
The frontier is not a chart about RTGS; RTGS merely stands at one end of it - liquidity parked, delay near zero, which is why lesson 3 found it moving enormous value in few transfers and lesson 4 found the funding bill. Module 0’s clearing house pattern lives at the other end: collect payments all day, cancel offsetting flows, settle only the differences - a whisper of liquidity, hours of delay. Lesson 7 industrialises that end as the batch rail, and lesson 9’s instant rails make their own choice, pre-positioning liquidity around the clock so small payments settle in seconds. The module milestone will place all three engines you build on this exact chart as measured points. From here on, meet every real rail with the frontier question: where on the curve did its designers choose to stand, and who pays for the choice - the banks parking liquidity, or the payees accepting delay?
Review
Queue instead of bounce
A bank that cannot cover a payment right now does not have to reject it. Rejecting punishes a timing problem as if it were a solvency problem, when the money is coming, just not yet. So the rail holds the payment instead, and two choices in the drain do the real work. First in, first out per paying bank rather than across the whole system, so one bank’s stuck payment does not jam another bank’s ready one. And a rescan after every success, because each settlement delivers reserves to some payee bank, and that delivery may cover a payment queued earlier in the line. Reserves recycle, and the loop chases the recycling until it runs out.
The frontier, and choosing a point on it
Hold the payment set fixed, sweep the opening reserves across ten levels, and measure the mean wait at each. The runs trace a curve: steep where reserves are starved, flat where they are ample. That is the liquidity-delay frontier, and the important part is that every rail in this module is a point on it, chosen by its designers rather than suffered. The heavy-duty rail parks liquidity to make delay vanish. The batch rail accepts hours of delay to make liquidity almost vanish. Nothing anywhere on that curve gets both for free.
Why every payment stays in the average
Counting every payment is the measurement’s integrity, not a detail. Drop the ones still stuck at the close and the starved runs look fast, because only the payments that found their moment remain in the average. That is the survivorship trick of a latency dashboard that excludes timed-out requests: it flatters exactly the configuration that failed the most. A payment still stuck after the close is charged its full wait, and the exercise makes you assert that settled plus stuck equals three hundred before you divide by anything.
Check yourself
1. mean_delay charges a payment stuck at the close its full wait to minute 1,440. What would dropping stuck payments do to the 250-per-bank row, and why is that number a lie?
The mean would fall, because the 34 payments that waited longest - and never settled - would leave the average, and only the payments the starved system found easy would remain. The configuration that failed the most payments would print the most flattering delay: survivorship bias, the same as a latency dashboard that excludes timed-out requests. Counting all 300, settled or stuck, is what makes the frontier honest.
2. The queue is first in, first out per paying bank rather than one global line. What breaks with a single all-banks head?
The first bank that cannot cover its oldest payment blocks the head, and every other bank’s perfectly ready payments jam behind it - one short bank stops the entire rail. Per-bank FIFO keeps each bank’s own promises in order while letting the other banks proceed. What it cannot fix is the day the banks block each other in a circle, each waiting to be paid before it pays; lesson 6 builds that day.
3. At 250 per bank the whole system posts 1,500 in reserves, yet 266 payments settle. What makes that arithmetic possible, and which assert proves nothing was minted?
Recycling. Every settlement delivers reserves to some payee bank, and drain immediately rescans for payments that delivery now covers, so the same unit of reserves settles payment after payment through the day. The run’s conservation assert proves the other half: world.total_reserves() still equals opening reserves times six after every day, because gross settlement relabels reserves between banks and never mints or destroys them. The system does not need much liquidity; it needs its liquidity to keep moving.
4. build_world endows each bank’s customer with the swept opening reserves, then lends the same customer 100,000. Why lend the rest instead of endowing it?
Endowment is the central bank’s act: it mints reserves and deposits together, on both tiers (module 1, lesson 10), so endowing 100,000 would flood every bank with settlement liquidity and there would be no frontier to see. Lending creates the deposit without touching reserves (module 1, lesson 9), so customers can afford every payment while the bank’s settlement liquidity stays exactly the knob the sweep is turning.
5. The 2,250 row and the 2,500 row both print a mean delay of 0.0. Say precisely what the extra 1,500 of system liquidity bought.
Nothing. At 2,250 per bank every payment already settles the minute it arrives; delay is zero and has no lower value to reach. The extra 250 per bank sits on the central bank’s ledger from open to close and touches no payment. The flat tail is the overprovisioning region, and real rails spend real money to stand in it - which is why the next lesson is about standing further left safely.
Do this
Fifteen minutes, from module-02-domestic-rails; standard library plus the canonical miniledger import, which the script resolves itself. Open code/liquidity_frontier.py: the queue wrapper, the seeded day and the ten-level sweep are all written. Yours is the measurement - mean_delay() is the TODO(you). Sum the waits of settled payments from their (arrival, settled) pairs, charge every still-queued payment its full wait from arrival to minute 1,440, assert that settled plus stuck is exactly 300 with a message saying why none may vanish, and divide.
python3 code/liquidity_frontier.py
The run prints the full ten-row table this lesson excerpted, then two asserts check the shape of your frontier: mean delay must never rise as reserves rise, and the two ends must sit far apart - starved reserves cost more than an hour, ample reserves cost minutes. Done right, the run ends with the line
post less liquidity and the same payments wait longer: reserves parked and delay accepted trade off along a measurable frontier
If your mean comes out suspiciously low at the starved levels, you almost certainly measured only the settled payments; the 34 stuck ones at 250 per bank are the expensive tail the measurement exists to catch. The completed version is solutions/liquidity_frontier.py; compare after you are green, not before.
What you can now do. You can put numbers on the trade every payment system lives inside: hold the day fixed, sweep the reserves posted, and price what each level buys - a 77.6-minute mean wait at 250 per bank, 3.2 minutes at 1,000, zero at 2,250 and nothing more forever after. You can read any rail as a chosen point on that curve and name who pays for the choice. And you know the queue that drew the curve has a failure mode the sweep never triggered: banks blocked in a circle, each waiting to be paid before it can pay, where no amount of rescanning makes progress. Lesson 6 builds that jam deliberately - and clears it with almost no liquidity at all.