Mempool scaling report

Praos (Leios not enabled) · 5,000,000 B mempool override · MempoolTimeoutCapacity 20 s

3 workloads, one per region, at 36 tx/s each (13.6 KB/s), 108 tx/s aggregate (40.9 KB/s)

Run 2026-08-26-20-33-6deff--value-leios-nomadperf-coay-nom · time zero 2026-08-26 21:59:03 UTC (the cluster's first log timestamp) · the generator's first trace at +6:11 · full scheduled duration, ended by cluster shutdown (no starvation) · 132,048 funds split across 3 workloads, one builder per region (EU node-6, US node-13, AP node-2), 44,016 each · 2 destination keys per builder · confirmation depth 3

Can a Cardano node run a 5 MB mempool, 28× the default, under a deliberate worst-case load (three disjoint per-region tx streams saturating it), and what does that cost Praos block production? Measured against an identical default-mempool control; the data is below, the conclusions in Section 5.

1. Setup

1.a Cardano Node code @ 3e1bec0

cardano-node 11.1.0.164, from leios-prototype commit 3e1bec0217b1560827956d5973120bbff983ee96 · running in Praos mode: Leios is not enabled · MempoolCapacityBytesOverride 5,000,000 B (Consensus-effective 5,078,016 B) · MempoolTimeoutCapacity 20 s (node default 5.0 s), a second capacity dimension alongside bytes: the pool is full when the cumulative validation-time of its resident txs reaches this, or when the byte override is reached, whichever binds first (at 5 MB the byte cap binds first, so this 20 s is not the binding constraint here) · MempoolTimeoutSoft 1 s (default), MempoolTimeoutHard 1.5 s (default) · maxBlockBodySize 90,112 B (Mainnet's value) · AWS EC2 c5d.2xlarge, x86_64 · memory 16,285,147,136 B (15.2 GiB) · kernel 6.12.30 · fleet CPUs: Intel Xeon Platinum 8124M @ 3.00GHz, 8223CL @ 3.00GHz, 8259CL @ 2.50GHz, 8275CL @ 3.00GHz

Trace configuration, checked when this report is built

Building this report fails unless every node namespace it consumes is configured with a passing severity, at least the needed detail level, and no frequency limit; checked against each target node's own TracerConfigInfo dump, all three verified identical. A capped cross-check stream is accepted only after scanning every node log and finding no limiter notice, so the cap demonstrably never fired.

1.b tx-centrifuge code

The load generator: a closed-loop workload driver. A workload is a self-contained circuit: its own disjoint partition of funds (single-use UTxOs), its own builder turning funds into txs, its own token-bucket rate limit, one injection target and one observer following the chain. Workloads share no funds, no txs and no queues, so each region's loop runs, and can fail, independently. Every tx consumes funds and creates outputs; an output becomes a spendable fund again only once the observer has seen its tx reach the configured confirmation depth (on_confirm recycling), so the txs form chains gated by the chain itself, and the funds budget is the buffer that bridges confirmation latency. That buffer is also the alarm: with on_exhaustion = error, a workload that cannot recycle funds as fast as it spends them runs the buffer to zero and fails the run loudly instead of quietly degrading. Between those two borders each workload keeps two buffers, funds waiting to be built (the funds queue) and built txs waiting to be delivered (the payload queue, cap 8,192)

This exact setup: 3 workloads, one per region, each targeting its in-region node: EU · node-6, US · node-13, AP · node-2 · funds 132,048 total, 44,016 per workload · rate limit: token bucket at 36 tx/s per workload, 108 tx/s aggregate (40.9 KB/s at the 379 B announced size) · every tx 2-in 2-out, two witnesses (2 destination signing keys per builder); one tx, three sizes: 371 B raw CBOR, 374 B in the mempool's accounting (CBOR + 3), 379 B announced on the wire (CBOR + 8) · recycling on_confirm at confirmation depth 3 · on_exhaustion = error

Trace configuration, checked when this report is built

Building this report fails unless every tx-centrifuge namespace it consumes is configured with a passing severity, at least the needed detail level, and no frequency limit; checked against the run-script's TraceOptions.

1.c Topology and load placement

51 pools · 3 regions: 17 × EU eu-central-1 (Frankfurt), 17 × US us-east-1 (N. Virginia), 17 × AP ap-southeast-2 (Sydney) · torus-dense · 306 producer entries = 204 links (102 reciprocated) · 153 intra-region links, 51 per region · 51 inter-region links, 17 per region pair · tx-centrifuge on the explorer machine (eu-central-1): not a pool, its only links are its three injection arrows · p2p graph properties (undirected, over the 51 pools): every pool has exactly 8 distinct link partners (6 outbound producers + 6 inbound, 4 of them mutual) · diameter 4, and every node's eccentricity is 4, so radius 4 too · mean shortest path 2.48 hops · the topology is static over the P2P stack: peer sharing is disabled (ncPeerSharing = PeerSharingDisabled in the node's configuration dump) and each node's producer set is fixed by its topology file, so a node talks only and always to its listed producers

Full graph

Intra-region links only

Inter-region links only

Node positions, the tx-centrifuge diamond and its injection arrows are identical in all three views; hover a node to highlight its links everywhere and see its degrees. Region colors everywhere: EU green, US red, AP blue. Arrows: the links tx-centrifuge injects txs over, one per target, 36 tx/s requested in this run; each target also serves as its workload's chainsync observer (confirmation depth 3 in this run). Links are drawn undirected; direction and per-node producer lists are in the adjacency table.

Adjacency table

1.d Latency

cardano-cli ping round-trips over the node-to-node handshake, measured at cluster start by each pool's healthcheck against its 6 producers: 306 directed links × 3 samples each = 918 measurements. Milliseconds.

Not an ICMP ping: cardano-cli ping opens a TCP connection and runs the node-to-node handshake protocol, so each sample is an application-level round-trip through both nodes' network stacks and mux, which is what the Cardano protocols actually experience; a raw ping, where ICMP is allowed at all, measures kernel-level echo with no TCP and no node in the path. The TCP connect column is the bare TCP round-trip from the same tool, the closest available stand-in for a raw ping; the gap to the protocol mean is a consistent ~0.3 ms on every link class, the protocol and scheduler overhead. SEM = SD / sqrt(n). The explorer's healthcheck has no producers to ping, so there are no direct tx-centrifuge measurements; the generator sits in eu-central-1, so its target RTTs follow the EU rows: node-6 within EU, node-13 EU to US, node-2 EU to AP.

2. Results

Everything measured from the run, in three views: Workload (tx-centrifuge), the generator's side of the closed loop, funds, deliveries and demand, from the tool's traces alone · Node mempool, each target pool's size and flows, from the node's Mempool traces alone · Chain, where the two sides meet and the blocks decide: composition, block fill and each workload's share of what was included

Traces and method · how every chart is drawn

All traces in this run are unfiltered (per event): ~3.1 M queue-depth samples, ~2.4 M mempool-size samples, and a per-event stream behind every rate. Depth-style series are drawn as a shaded min-to-max band per 42 s bucket with a 2 px line through each bucket's final value, so extremes survive exactly and oscillation reads as band thickness. Rates use fixed bins drawn as steps, 30 s in the 30-minute left charts and 300 s in the full-run right charts; the two mean-speed rows are running since-first-send averages, exact at every event. Nothing is smoothed. Every chart row is split at 30 minutes: the ramp on the left from zero, the rest of the run on the right, with right axes clipped to the steady range where noted. The crosshair links both halves and all sections; lines interpolate between samples; hover or focus a chart and use arrow keys to read the last observed sample per series. The dropdown under each row names that chart's exact traces and computation.

2.a Workload (tx-centrifuge)

The generator's side, tx-centrifuge traces only: circulating funds (the closed loop at work), cumulative delivered bytes, the mean delivery speed, and the unserved demand at each target's tx-submission server

What this run demonstrates about tx-centrifuge. A fixed, finite budget of 132,048 UTxOs ran a closed loop for 17 hours 39 minutes with zero fund loss: the funds ledger closes two independent ways, from the border flows (ReplyTxs deliveries minus Recycler returns) and from the queue depths (the Pipe traces), agreeing exactly at the builder border and to within ~200 in-transit funds at the network border. Pull-based delivery never overran a target, and the Unserved demand row is the receipt: because the node pulls, demand is explicit on the wire, and the run shows each workload's rate control independently at work: node-6 re-asks 41,533 ids while its 36 tps bucket caps the fill, node-2 never accumulates more than 19 because its 250 ms RTT already paces it below the bucket, and every curve goes dead flat the moment the pools fill: three independent loops, each visibly finding its own equilibrium. Forks do not shake it either: this run's height-battle rate is ten times its control's (.04968 vs .00477 per block), yet the offered load never wavered, because an orphaned tx's funds are recovered and rebuilt automatically (10,882 orphan recoveries alongside 689,248 clean confirmations) and the pull loop keeps delivering at the chain's pace; the Unserved demand curves stay flat through every battle. It kept all three 5 MB mempools, all three payload queues and the confirmation window saturated for the entire run, was the source of every one of the chain's ~10.8 tx/s for 17 hours, and its fail-fast guard (on_exhaustion = error) stayed armed throughout and never fired. The design choice that makes these numbers benchmark-grade is node-to-node: the load rides the node's own TxSubmission protocol as a genuine peer, so it is pulled at each target's real pace over the real topology, exactly what the network experiences under load. A node-to-client pusher such as tx-firehose fills a single node's mempool directly through the local socket, ideal for localhost and single-node fills, but it bypasses relay topology and contends with block production on the block producer, so it cannot measure diffusion or RTT-paced pull. Closed-loop confirmation-depth recycling gives provable fund conservation and indefinite runs on a finite budget, and rides through forks in-process, as the orphan recoveries above show; the per-target token bucket and fail-fast guard make each region's load independent and its failure loud. The measurement comes off the same wire as the load. The Workload and Chain numbers come from its own traces, the mempool and hardware charts from the nodes' traces, and the composition row joins the two; the sides cross-check: replayed mempool contents match the nodes' counters, and delivered sizes match the mempool's accounting, to the fund, the tx and the byte.

Traces and method · Circulating funds

The closed loop measured at the network border: a queueing input-output counter, N(t) = funds sent minus funds returned, as a share of the workload's 44,016 funds. Sent: tx-centrifuge TxCentrifuge.TxSubmission.ReplyTxs (DDetailed needed for the txs list), counted as 2 funds per delivered tx with no builder join, and that is exact, not an estimate: the event's target identifies the workload by construction (each workload's tx-submission server feeds from its own payload queue and serves exactly one target, so a foreign tx cannot appear in the batch), and every tx in this profile consumes exactly 2 funds (inputs_per_tx = 2, a fixed builder shape; the pools staying pure 374 B two-witness all run verifies it never drifted). Only the tx count is used, never the fund lists. Returned: TxCentrifuge.Recycler.AddToPipe (DDetailed suffices, it carries count; this run logged DMaximum), minus that count, the number of funds released (confirmed outputs, or original inputs on orphan recoveries; the recycler handles only its own workload, no join needed; 700,130 releases vs 689,248 pure confirmations by the Announce cross-check). What circulates is in mempools, the confirmation window or transit; the rest of the budget sits tool-side in the payload buffer or the funds queue. Conservation: the counter equals 44,016 minus the funds queue minus twice the payload queue, to within ~200 in-transit funds at its worst transient, typically a few dozen (ends 28,508 / 25,412 / 21,596 measured vs 28,520 / 25,434 / 21,780 predicted). Display: 42 s min-max band with the bucket-final line, no smoothing.

Traces and method · Delivered bytes

Same TxCentrifuge.TxSubmission.ReplyTxs replies; the per-tx size fields (raw CBOR bytes, 371 B for a two-witness tx; the same tx announces as 379 B on the wire, the size the token bucket meters) accumulate into a running total per target. For a speed, the cumulative curve is the smoothing-free display: the slope is the delivery rate, ~13.4 KB/s of raw CBOR while the token bucket binds during the fill and a steady 1.59 / 1.32 / 1.24 KB/s (node-6 / node-13 / node-2) after it, a stall would show as a flat spot, and the end value is the total delivered: 105.2 MB to node-6, 86.8 to node-13, 81.7 to node-2. A moving average would lag and hide extremes; a since-start average would stop responding after the first hours. Display: 42 s band with the bucket-final line, no smoothing.

Traces and method · Mean delivery speed

The running average speed, recomputed at every delivery: total raw CBOR bytes sent to the target so far, divided by the time elapsed since that workload's first send. No window, no weighting, no smoothing; every point is exact. The curve starts at the token bucket's rate while the bucket binds (~13.4 KB/s during the fill), then eases toward the whole-run average as steady state accumulates: it ends at 1.66 KB/s to node-6, 1.37 to node-13, 1.29 to node-2. The dashed gray curve is the same running average over the three targets combined, from a 33.0 KB/s fill peak to 4.31 KB/s at the end. Read it with the Delivered bytes row above: this is that curve's height over elapsed time.

Traces and method · Unserved demand

A cumulative counter per target: every tx-centrifuge TxCentrifuge.TxSubmission.RequestTxIds adds its req, the number of new tx ids the target asks for (DNormal suffices, it carries ack, req and target; 310,418 requests), and every TxCentrifuge.TxSubmission.ReplyTxs batch subtracts the txs it delivered. The height is every ask the generator could not serve at ask time: during the fill the 36 tps token bucket paces replies below what a fast target asks, the target re-asks, and the excess accumulates, node-6 to 41,533 ids (asked 325,189, delivered 283,656) and node-13 to 4,889 in the first minutes, while node-2's 250 ms RTT already paces its asks and its backlog peaks at 19 and ends at 12. After the fill every curve is flat to the end: in steady state every ask is served (starvation, the failed run's QueueStarved signature, would climb here). Display: 42 s min-max band with the bucket-final line, no smoothing.

2.b Node mempool

The nodes' side, Mempool traces only: pool size, and the pool's ingress and egress at each injection target

Traces and method · Mempool size, bytes

Node traces Mempool.AddedTx, Mempool.RejectedTx and Mempool.RemoveTxs, selected by namespace only (~2.4 M events; DNormal suffices, the node default here). Every one carries mempoolSize.bytes, the pool's own accounting after the operation (txInBlockSize = sizeTxF + perTxOverhead 4, about raw CBOR + 3; 374 B per two-witness tx). Display: 42 s min-max band with the bucket-final line, no smoothing.

Traces and method · Mean ingress speed

Node Mempool traces only. Every Mempool.AddedTx event carries mempoolSize.bytes after the operation, so the rise over the previous mempool event's value is exactly the accepted tx's bytes entering the pool; rejected deliveries change nothing and are excluded by construction. Ingress counts every accepted arrival, the workload's own deliveries and foreign txs diffusing in from peers alike, which is why the fill-phase peaks (up to 27 KB/s at node-13) exceed the tool's 13.4 KB/s delivery pace. Running average since the first add (+0:07:13). Ends: node-2 4.14 KB/s (262.7 MB), node-13 3.59 (227.8 MB), node-6 1.94 (122.7 MB); ingress minus egress equals the pool's resident bytes at its last event, to the byte.

Traces and method · Mean egress speed

Node Mempool traces only. Every Mempool.RemoveTxs event carries mempoolSize.bytes after the operation, so the drop from the previous mempool event's value is exactly the bytes an adopted block claimed from this pool; no join and no tx-centrifuge data. This is egress from the mempool, not from the chain: all three nodes adopt the same full blocks at the chain's ~4.06 KB/s (in the mempool's 374 B measure), so a block-level egress would be identical everywhere; what differs per node, and what this row measures, is how much of each block was actually resident in its pool. Running average since the first removal (+0:07:48), the same construction as the Workload section's Mean delivery speed. Ends: node-2 4.06 KB/s (257.7 MB, virtually the whole chain passed through its pool), node-13 3.51 (222.8 MB), node-6 1.86 (117.8 MB, barely half: its ~99% own-workload pool never held most of what the chain included; the blocks that remove anything claim ~137 of their ~238 txs from it, ~109 averaged over all 2,896 blocks, since ~600 remove nothing). The dashed line on both speed rows marks the ~4.06 KB/s whole-chain rate.

Traces and method · Rejected txs

Node Mempool traces only: every Mempool.RejectedTx event names exactly one tx.txid, so the counter adds one per event (verified: events equal txids on all three nodes). How a rejection happens: a tx offered over tx-submission is validated before entering the pool, and the offer is refused when the pool already holds that txid or when its inputs are no longer available because a resident or already-confirmed tx consumed them; duplicate offers of the same tx arrive over parallel peer paths, and stragglers keep arriving after confirmation. A full pool by itself does not reject: an add attempt blocks up to MempoolTimeoutHard (1.5 s) for space, then disconnects. A rejection is a refused offer, not an eviction: nothing enters or leaves the pool. How it is accounted in this report: this counter adds one; the size charts above take the event's unchanged after-event mempoolSize as one more sample; and the ingress and egress rows add nothing, because the event's byte delta is zero, which is exactly what keeps ingress minus egress equal to the resident bytes. Rejections start with the very first deliveries (+0:07:13) and end at 401,313 for node-2, 325,976 for node-13 and 69,767 for node-6; the order tracks pool mixing: the most-mixed pool refuses the most re-offers, node-6's ~99% own-workload pool the fewest.

2.c Chain

The arbiter, where the two sides meet: the composition row joins node mempool events to the generator's build map, the block and share rows come from the generator's chain observer, and a node block-fetch trace cross-checks them · 84.1% of blocks are single-workload snapshots; every block is full; the cumulative split is 38.3 / 32.1 / 29.6% (EU / US / AP)

Traces and method · Mempool composition

In three steps. One: the generator logs every tx it builds, TxCentrifuge.Builder.NewTx carries builder and txId, which gives a dictionary from txId to the workload that built it (DNormal suffices; this run logged DMaximum). Two: each node logs every pool change, so replaying Mempool.AddedTx (insert that txid) and Mempool.RemoveTxs (delete the txids it lists) in log order reconstructs the exact set of txids resident in the pool at every moment; Mempool.RejectedTx changes nothing (1.65 M events replayed). Three: at each moment, look every resident txid up in the dictionary and divide: own-share % = residents built for this node over all residents, recorded as the last value per 42 s bucket. Details: node-side txids are truncated to 8 hex digits at DNormal, so the dictionary matches on the first 8 digits; this is set replay, not sampling, and the replayed set sizes match the nodes' own mempoolSize counters throughout.

Traces and method · Block fill

The TxCentrifuge.Observer.Announce stream (the observer following node-6's chain) clustered into blocks: announcements arrive in per-block bursts spread over milliseconds while blocks sit ~20 s apart, so a gap above 0.25 s starts a new block. 2,896 blocks over the run, one every 21.9 s on average; the seven bursts of exactly 476 txs are back-to-back full blocks closer than 0.25 s, split back into their two blocks in announce order. Bytes per block = announced txs × 374 B, exact because every tx in this run is a verified 374 B two-witness tx; full = 238 txs = 89,012 B, the most that fits under the 89,088 B effective block body, drawn dashed. Every one of the 2,896 blocks is full, from the very first at +0:07:16 to shutdown: the chain ran at capacity for the entire run. The left chart keeps the from-zero scale; the right is zoomed to a 500 B window, so the flat line and its 76 B headroom under capacity are visible, and a not-full block would plunge off the bottom edge; none does, and no burst falls below 238 announced txs, so the gap rule split nothing. 84.1% of blocks carry txs from a single workload (the median block: all 238 from one forger's own-workload pool). Node-side cross-check, from a direct node trace: BlockFetch.Client.CompletedBlockFetch carries each downloaded block's wire size. Its dominant value is a constant 88,457 B, the full block on the wire (3,712 of node-6's 4,063 fetches), and its 18 sub-kilobyte entries are the empty blocks forged before the generator's first delivery, which the announce stream cannot see; the fetch stream also holds fork and duplicate downloads, which is why its count exceeds the chain's 2,896. The stream carries a 2 events/s config cap, and the build verifies no limiter notice exists in any node log, so it is complete.

Traces and method · Cumulative chain share

In three steps. One: the same txId-to-workload dictionary from Builder.NewTx. Two: the observer following node-6's chain logs one Observer.Announce per tx entering an adopted block, the complete on-chain stream in order (689,248 announcements). Three: three running counters, one per workload; every announcement looks its txId up in the dictionary and adds one to its builder's counter, and the chart plots each counter over the sum of all three, per bin. The counters end at 264,018 / 221,370 / 203,860 (EU / US / AP), which is 38.31 / 32.12 / 29.58%, within about five points of equal thirds. Raw shares are composition, not chain preference: a workload whose funds return faster builds, sends and therefore lands more txs, and the loop feeds back, since income is inclusion. The chain-side check is included over delivered per workload: 264,018 / 283,656 = 93.1% EU, 221,370 / 233,971 = 94.6% US, 203,860 / 220,261 = 92.6% AP, equal within two points; the remainder is txs still in mempools or the confirmation window at shutdown, plus orphaned txIds rebuilt under new ids. The chain converts what every workload delivers at the same rate; the EU volume edge is the co-location edge, its funds return fastest, while the forging lottery is uniform over the 51 pools.

Traces and method · Chain density

Node traces only: LedgerMetrics carries chainDensity, the node's own reading of its selected chain's density, emitted about once per slot (~63,700 samples per node). All three nodes report an identical series, as followers of the same chain. The value is windowed over the node's recent chain, which is why it swings between .031 and .073 rather than settling on the whole-run .0457. The dashed line is the configured activeSlotsCoeff, 0.05; monitoring dashboards often show the same quantity normalized to it, where 0.05 reads as 100% and this band as 62% to 146%. Display: 42 s min-max band with the bucket-final line, no smoothing.

3. Comparison against the default-mempool control

Three runs side by side: the vanilla 11.0.1 run, the leios-prototype control without the mempool overrides, and this run with them, +5MB mempool · per metric: each run's average, the control's delta against 11.0.1, and this run's delta against the control · the Summary table is run data, without deltas · both leios runs were driven by tx-centrifuge at 108 tps aggregate: the Summary's TPS 12.0 and Transaction count are the profile's nominal tx-generator values, not the load that ran.

3.a Chain totals

Traces and method · Chain totals

One row per run, each built from that run's own files. The 11.0.1 row is the one piece here that is not part of the standard flow: a vanilla, non-leios-branch node build from an earlier campaign, kept for orientation. Run time is locli's "Run time, s" from summary.org. Total slots and blocks are the raw totals in the locli filter line of blockprop.control.org. Chain density is blocks over slots, read against activeSlotsCoeff 0.05. ChainDB's SwitchedToAFork events (each time a node's chain selection jumps to a competing chain) exist only for the three target nodes whose logs ship with the run bundle, a sample, not the cluster: this run 152 / 154 / 157, median 154; the control 68 / 57 / 60, median 60; the vanilla run ships no node logs at all. locli's Height & slot battles row, computed over all 51 hosts' logs, is in the Control sample table below. The per-node density over time is the Chain density chart in 2.c. The rows are, in order: 2026-05-06-08-27-6ca87-1101-value-voltv11-nomadperf-coay-nom, 2026-08-28-14-00-4cd78-defaultmempo-value-leios-nomadperf-coay-nom, 2026-08-26-20-33-6deff--value-leios-nomadperf-coay-nom; both leios runs are the same cardano-node build, and the short prefixes in the run names tag the run, not the node. For this run the 2,914 raw blocks exceed the body's announce-based 2,896 by the 18 empty blocks forged before the generator's first delivery, which the announce stream cannot see. locli's epoch3+ cut removes blocks 0-804 / 0-730 / 0-722 (11.0.1 / control / 5 MB) and its further in-window exclusions are 156 of 2,400 (6.5%) / 79 of 2,383 (3.3%) / 138 of 2,191 (6.3%), for filtered ratios of 0.70 / 0.73 / 0.70.

3.h Observations

1. Forge and adoption cost. A header leaves the forge ~20× later (slot start to announced .073 to 1.517 s) and half-cluster adoption takes ~3.3×, +232% (0.50 adoption .626 to 2.077 s). The cost is forge-side, not wire: fetch duration is flat across all three runs (.348 / .354 / .358 s), so the blowups are origin-side, the forge loop working through a full pool, not diffusion.

2. Corroborating costs. Height and slot battles rise ~10× (.0048 to .0497 per block). Chained to forged blocks: .966 / .964 / .900 (11.0.1 / control / 5 MB): the 5 MB run orphaned about one forged block in ten, against about one in thirty for the other two. Chain density falls ~7% (.04904 to .04575) against the 0.05 target. Average CPU is ~3.5× (3.35 to 11.73%), while CPU 85% spans fall to 0.55 slots against ~7 in the other two runs: a full pool is sustained reapply load, not bursts.

3. Caveats. Per-stage forge timings are not comparable across builds: vanilla 11.0.1 books ~48 ms as Mempool snapshotting and ~0 in the forge steps, while the leios-prototype books ~0 snapshotting and folds that cost into the forge steps (Leadership to forged reads .0008 s for 11.0.1 vs .0505 s for the control, the same ~48 ms re-labeled); compare the control against 11.0.1 only on aggregate forge and adoption metrics, never stage by stage. The control was also saturated: its default pools (two blocks, ~180 KB) ran full under the same 108 tps load, and all three runs make full ~88.5 KB blocks, so the comparison isolates mempool size at saturation, not saturation itself. The two leios runs share the node build, generator, load and topology and differ only in the mempool configuration (MempoolCapacityBytesOverride 5,000,000 B and MempoolTimeoutCapacity 20 s against the node defaults), so the Δ vs leios-prototype column isolates the mempool overrides, while Δ vs 11.0.1 crosses node builds and generators (11.0.1 with tx-generator at 12 tps): context, not attribution.

4. Reading

The run completed its full 17 h 39 m schedule and ended by cluster shutdown; no starvation event fired. All three mempools pin at exactly 13,577 txs = 5,077,798 B of pure 374 B two-witness txs. Composition: node-6's pool holds ~99% own-workload txs throughout, and the blocks that remove anything take ~137 of their ~238 txs there (~109 averaged over all blocks; ~600 remove nothing); node-2 and node-13 oscillate between ~28% and ~80% own share; the union across the three pools stays near 27k distinct txs. Every one of the 2,896 blocks is full at 238 txs, one every ~22 s. 84.1% of blocks carry txs from a single workload, and the median block holds all 238 from one workload. The cumulative inclusion split ends at 38.3 / 32.1 / 29.6% (EU / US / AP); included over delivered per workload is 93.1 / 94.6 / 92.6%.

5. Conclusions

Scope. Praos, Leios not enabled: this run measures the mempool state and the block-production cost of a scaled mempool, not Leios certification. One run per configuration, with an identical default-mempool control on the same hardware and time window. The load is a deliberate worst case for mempool divergence (three disjoint per-region tx streams), not a model of Mainnet's overlapping demand.

1. A 5 MB mempool ran without failing. The node held a 5,078,016 B mempool (28× the default, 13,577 txs) full for the entire 17 h 39 m schedule, ending by cluster shutdown, not starvation: heap flat, funds conserved to within ~200 in-transit at any instant, the fail-fast guard never fired. The budget's margin was thin: node-6 finished at exactly 0 spare funds (~1% headroom overall), so the next run should size the fund budget up. Scaling the mempool this far is mechanically safe under Praos.

2. The cost is the mempool size, and the control attributes it. All three runs produce full ~88.5 KB blocks, so the throughput is the same and the difference is forge cost. Against the identical default-mempool control (same build, generator and load), the 5 MB pool cost roughly 3× block adoption (0.63 to 2.08 s at the median), a header leaving the forge ~1.5 s into the slot (0.07 to 1.52 s), ~10× the height and slot battles (.0048 to .0497), a fall in blocks that reach the chain from .96 to .90 (one forged block in ten orphaned), ~7% lower chain density, and ~3.5× CPU. The cost is forge-side, not diffusion: fetch duration is flat across all three runs (~0.35 s). The 1.52 s forge time exceeds the 1 s slot length: the block's header is announced more than a slot after the slot began. This is the computational backlog at the nodes that saturated all-producer runs exist to expose, now large enough to affect slot timing. Total adoption still sits within the "95% of nodes in 5 s" diffusion target (1.00 adoption 2.48 s), so the security budget is not breached, but the origin-side backlog consumes a large share of it and shows in the degraded chain quality above (the ~10× battles and ~1-in-10 orphaned blocks). It is the pool size, not the prototype build: on the aggregate forge and adoption metrics the control matches vanilla 11.0.1, so the prototype adds no measurable overhead at the default mempool and this cost should generalize to a release node (the per-stage forge breakdown is re-attributed between builds and is not compared stage by stage). The workload is value-only, the cheap case for reapplication; a script-heavy mempool of the same size would cost more. The delta against vanilla 11.0.1 crosses build, generator and load, and is context, not attribution.

3. The imposed fragmentation did not reach the chain. The load holds the mempools maximally divergent by construction: three disjoint per-region tx streams, so the co-located EU node holds ~99% its own region's txs while the two remote nodes oscillate between ~28% and ~80% own share, the union across the three pools is about two disjoint pools' worth, and 84% of blocks carry a single workload. Under Praos this did not affect the chain: every block full, inclusion fair to thirds (38.3 / 32.1 / 29.6%), each region's txs converted at the same rate (93.1 / 94.6 / 92.6% included over delivered). In Praos a block carries its own transactions, so mempool divergence cannot threaten validity or fairness once block production is regionally balanced. This is a Praos property and does not carry to Leios, where an endorser block references transactions its voters must already hold.

4. For Leios, this bounds the input and leaves the consequence open. The measured divergence (union ≈ two mempools) is a data point for TxCache sizing, and the forge-side slowdown under a full pool draws on the same budget an endorser block's validation would compete for. Whether fragmentation of this magnitude prevents endorser blocks from certifying within their vote deadline is not testable in Praos; only a Leios-enabled run measures it. This run shows the fragmented state is real and reachable with three regional load sources; it does not show whether Leios survives it.

Next. Enabling Leios (endorser blocks on) to test finding 4; independently, a TxSubmission V1-versus-V2 comparison (a separate knob that may or may not be combined with the Leios runs); repeats to put error bars on findings 1 and 2; and a node-local, coloring-free divergence metric (block-hit-rate) to carry the finding-3 measurement onto testnets.

Appendix A. Misc

Both sides' leftovers: the tool's internal buffers from tx-centrifuge traces, funds waiting to be built (the funds queue) and built txs waiting to be delivered (the payload queue), plus CPU and RTS heap from the node Resources trace

Traces and method · Funds queue

tx-centrifuge traces TxCentrifuge.Pipe.InputsEnqueued and TxCentrifuge.Pipe.InputsDequeued, unfiltered; DNormal detail suffices (depth and pipe; this run logged DMaximum) (~1.6 M events). Every event carries depth, the queue depth after its own operation, so merging both directions in time order gives the exact depth trajectory; time zero is the earliest timestamp across the generator and node logs. Display: per 42 s bucket, the band spans min to max and the line passes through the bucket-final value. No smoothing.

Traces and method · Payload queue

tx-centrifuge traces TxCentrifuge.Pipe.PayloadEnqueued and TxCentrifuge.Pipe.PayloadDequeued, unfiltered; DNormal suffices (~1.5 M events); same exact-depth method and 42 s band display as the funds queue. The 8,192 cap is the TBQueue size hardcoded in the tx-centrifuge Config.Runtime.

Traces and method · CPU

Node trace Resources, field CentiCpu, a cumulative CPU centisecond counter. Per-sample % is the counter delta over wall time between consecutive 1 s samples; the band is the min to max of those within each 42 s bucket, and the line is the true bucket mean, the counter difference across the bucket over its duration. Rates from a counter, no smoothing.

Traces and method · RTS heap

Node trace Resources at 1 Hz; every field ships at any detail level (~63 k samples per node), field Heap in bytes. Display: 42 s min-max band with the bucket-final line.

Reading (buffers)

Payload queues reach the 8,192 cap within minutes and stay there for the rest of the run; with the mempools full, targets accept new txs at the chain's replacement rate, ~10.8 tx/s total, one ~238-tx block every ~22 s. The funds queues diverge after the fill: node-6 stays near the floor for the whole run (post-fill maximum 1,570, ending at 0), while node-2 and node-13 hold idle funds in slow waves, up to 23,752 and 17,316 mid-run, ending at 2,198 and 5,852. Circulating funds: node-6 stays inside a 59 to 67% band all run; node-2 and node-13 dip to 9% and 23% mid-run. Recycle income: 269,402 txs returned to node-6's workload (~4.2/s), 223,173 to node-13's, 207,555 to node-2's. 44,016 minus the funds queue minus twice the payload queue matches the circulating counter to within ~200 in-transit funds at its worst transient, typically a few dozen.

Appendix B. Data table

Data table (last observed value per minute)

Last observed value by the end of each minute since start; · means no sample yet.