Load test results
Runs of the Control Center's load test (Load test in the sidebar) against the live server at propustech.tech, October 2026. Each run links to its raw report.
Setup
| Server | 4-core, 16 GB VPS shared with other services |
| Propus limits | 3 cores, 4 GB memory (systemd CPUQuota=300%, MemoryMax=4G) |
| Guard | New users pause above 80% host CPU or memory |
| Build | Single binary, all engines in one process |
| Data | A fresh database per run; the catalog, customers and coupons written by an AI model for a made-up business |
Workload
An online store and its back office on every engine at once:
- CoreStore: 18 tables (catalog, warehouses, stock and stock movements, carts, orders and lines, invoices, payments, coupons, loyalty, double-entry ledger, notifications) with row-level security per region. Checkout, ship, deliver, cancel and return are interactive transactions.
- FlashCache: product pages, carts, rate limits, stock locks.
- PulseStream: a relay reads the change feed into an orders stream; a notifier group writes notifications and fans out to email, SMS and push streams; senders retry three times, then dead-letter; an analytics consumer rebuilds order counts.
- ObjectCore: product images and invoice documents.
- TrackSense: product search kept in step with the catalog.
Users are shoppers (browse, search, cart, checkout, resubmit, cancel, return), staff (ship, deliver, receive stock) and managers (price changes, reports, trial balance), mixed 70 / 20 / 10.
Rules checked
| Rule | Holds when |
|---|---|
| no-oversell | Stock never goes below zero and reserved never exceeds on hand, even when many shoppers race for the last units |
| reserved-matches-open-orders | Reserved stock equals the quantities of paid orders not yet shipped |
| stock-matches-movements | Every warehouse's stock equals the sum of its stock movements |
| order-money | Each order's total equals its lines plus tax less discount, and equals its invoice and payment |
| ledger-balances | Debits equal credits, overall and for every posting |
| coupons | Each coupon's use count equals the orders using it and never passes its limit |
| loyalty | Each customer's points equal the points of their orders still standing |
| idempotency | A resubmitted order never creates a second order |
| tenant-isolation | A region's users never see another region's rows |
| cache-consistency | A cached value reads back the same; a lock is never held twice; documents read back intact |
| events-complete | Every order change reaches the stream once and makes exactly one notification |
| events-ordered | Events for one order arrive in the order they happened |
| deliveries-settle | Every notification ends delivered or dead-lettered |
| analytics-matches | Order counts rebuilt from the events equal the counts in SQL |
Results
Every run passed every rule, with no errors.
| Run (UTC) | Users | Transactions | Seconds | Avg / peak tx/s | Orders | Rule checks | Failed | Events published / delivered / dead | Ended |
|---|---|---|---|---|---|---|---|---|---|
| 2026-10-22 04:24 | 3,000 | 10,104 | 410 | 24.6 / 44.0 | 3,012 | 19,066 | 0 | 9,639 / 9,638 / 1 | Reached its 10,000 target |
| 2026-10-21 22:36 | 10,000 | 4,456 | 133 | 33.5 / 50.5 | 1,288 | 9,508 | 0 | 4,016 / 4,016 / 0 | Stopped, then checked |
| 2026-10-21 22:33 | 5,000 | 8,111 | 268 | 30.3 / 52.5 | 2,434 | 15,360 | 0 | 7,737 / 7,737 / 0 | Stopped, then checked |
| 2026-10-21 21:32 | 5,000 | 8,633 | 315 | 27.4 / 62.5 | 2,411 | 16,440 | 0 | 8,097 / 8,097 / 0 | Stopped, then checked |
The one dead letter is a sender giving up after three simulated failures, as designed.
The 3,000-user run
| Business | 3,012 orders · 2,194 shipped · 2,174 delivered · 790 cancelled · 1,469 returned · 340 price changes · 125 stock receipts |
| Refused correctly | 633 out of stock · 117 coupon at its limit · 1,615 resubmits blocked |
| Busy writes | 0 (retried writes all went through) |
| Cache | 5,963 hits, 3,673 misses |
| Events | 101,307 changes read · 9,639 published · 9,638 delivered · 308 retries · lag 0 at the end |
| Host CPU | 64% at most |
| Operation | Count | p50 ms | p95 ms | p99 ms |
|---|---|---|---|---|
| search | 4,867 | 2.0 | 8.9 | 17.3 |
| cart | 4,932 | 2.6 | 5.5 | 9.1 |
| my-orders | 3,192 | 2.2 | 5.9 | 8.9 |
| browse | 9,636 | 5.0 | 58.7 | 81.8 |
| reports | 430 | 59.5 | 270.6 | 466.5 |
| trial-balance | 421 | 197.9 | 453.8 | 576.1 |
| checkout | 6,422 | 4,208.6 | 5,877.1 | 6,931.8 |
| deliver | 2,890 | 4,192.3 | 5,759.9 | 7,014.3 |
| return | 1,472 | 4,032.6 | 5,667.2 | 6,654.5 |
What it found
Fixed before these runs:
random()was missing (42883); added.- Joins were nested loops (5.6 s on the report queries); CoreStore now plans hash joins for equality joins, checked against PostgreSQL.
- Typed literals (
DATE '2026-10-21',NUMERIC '1.5') were refused; now casts. - Under thousands of users the server ran out of threads; thread limits raised, spawn failures handled, a run that panics is contained.
- Writes waiting on the writer for 10 s failed with 55P03; they are now retried, then counted as busy.
- The CPU guard read nothing when systemd hid
/proc; the installer setsProcSubset=alland a run refuses to start without the readings. - Stop waited for in-flight users; it is now immediate, and Finish & check runs the final checks.
Limits shown
- Writes go through one writer. Reads stay in single-digit milliseconds, but write transactions wait their turn: about 25–60 business transactions a second, falling as users rise; checkout p50 is about 4 s at 3,000 users.
- Notifications lag under saturation. Delivery p95 was 5.2 minutes at 3,000 users (12 s in the first 5,000-user run). Nothing is lost; it catches up.
Next engine work: server-side transactions in one call (shorter writer holds), group commit, then row-level write concurrency.