Performance
Two figures say how fast the demo bank is, both read off the demo's
/about.json so a program can take them: customers added per second
(a batch add through the real pipeline: customer, accounts, register,
funding movements with projections) and account days per 12h (the
start-of-day pass's rate over the whole day, projected to a bank's
overnight run: every account visited once, its position for the day
written). The Hetzner runs are the numbers that matter; the laptop tables
below them are the baseline the design was measured against as it changed.
Hetzner runs
Made by gobank-deploy's perf workflow (tp secrets gobank-deploy perf perf): an environment is created at each scale, the demo set flat out,
customers added for ten minutes, days run for ten more, the rates read,
the servers removed. The command writes each run's row into this table
(small is cx23, shared; large is ccx33, dedicated cores, so the
account-days figure says whether the pass uses them); commit and release
to publish. The spans and the workflow's steps are on the gobank-deploy
Performance page.
| Date | gobank | Scale | Server | Customers reached | Customers/s | Days run | Account days / 12h | Last day |
|---|---|---|---|---|---|---|---|---|
| 2026-10-06 | v0.12.0 | small | cx23: 4 GB | 60,000 | 142.1 | 3 | 18,388,121 | 4m41s over 119,794 accounts |
| 2026-10-06 | v0.12.0-1-g479cbf4 | large | ccx33: 32 GB | 60,000 | 251.3 | 5 | 40,914,910 | 1m52s over 119,794 accounts |
| 2026-10-06 | v0.12.0 | small | cx23: 4 GB | 60,000 | 142.1 | 3 | 18,388,121 | 4m41s over 119,794 accounts |
| 2026-10-06 | v0.12.0-1-g479cbf4 | large | ccx33: 32 GB | 60,000 | 251.3 | 5 | 40,914,910 | 1m52s over 119,794 accounts |
| 2026-10-06 | v0.12.0 | small | cx23: 4 GB | 60,000 | 142.1 | 3 | 18,388,121 | 4m41s over 119,794 accounts |
| 2026-10-06 | v0.12.0-1-g479cbf4 | large | ccx33: 32 GB | 60,000 | 251.3 | 5 | 40,914,910 | 1m52s over 119,794 accounts |
| 2026-10-07 | v0.18.0-2-g3c9905f | small | cx23: 4 GB | 3,000 | 2.6 | 15 | 7,426,830 | 37s over 6,047 accounts |
| 2026-10-07 | v0.18.0-2-g3c9905f | large | ccx33: 32 GB | 4,000 | 5.0 | 26 | 13,367,814 | 34s over 8,060 accounts |
For comparison, the Hetzner demo of 2026-10-01 (300k customers, v0.3.x, one accrual posting per account per day) took over ten minutes a simulated day at about 1.2k postings/s.
Laptop baseline (pglike)
Run 2026-10-06 on Intel Core Ultra 7 165H, Linux, Go 1.26.0, pglike (SQLite
:memory:) backend, at the stage 4 (c) branch (what became v0.15.0):
positions as the truth, the start-of-day pass, the simulation a package
of its own. task bench:days and task bench:baseline reproduce them.
Day-scaling (real customer pipeline, pglike)
Uses generateCustomer with seed 42 — each customer gets 1-3 accounts randomly.
Pre-populated customers, then simulation advances day-by-day with interest accrual,
BoE rate lookup, history recording, and go-luca ledger movements.
| Customers | Accounts | Days | Total | us/day | acct-days/sec | Allocs |
|---|---|---|---|---|---|---|
| 1 | 3 | 7 | 27ms | 3,898 | 770 | 94K |
| 1 | 3 | 30 | 124ms | 4,147 | 723 | 405K |
| 1 | 3 | 60 | 255ms | 4,256 | 705 | 823K |
| 1 | 3 | 180 | 755ms | 4,199 | 715 | 2.5M |
| 1 | 3 | 365 | 1.5s | 4,157 | 722 | 5.0M |
| 10 | 22 | 7 | 128ms | 18,353 | 1,199 | 456K |
| 10 | 22 | 30 | 573ms | 19,116 | 1,151 | 2.0M |
| 10 | 22 | 60 | 1.1s | 18,403 | 1,195 | 4.0M |
| 10 | 22 | 180 | 3.3s | 18,315 | 1,201 | 11.9M |
| 10 | 22 | 365 | 6.5s | 17,728 | 1,241 | 24.1M |
| 100 | 198 | 7 | 1.0s | 143,921 | 1,376 | 3.8M |
| 100 | 198 | 30 | 4.2s | 140,028 | 1,414 | 16.2M |
| 100 | 198 | 60 | 8.1s | 135,512 | 1,461 | 33.0M |
| 100 | 198 | 180 | 22.0s | 122,222 | 1,620 | 98.7M |
| 100 | 198 | 365 | 41.6s | 114,049 | 1,736 | 200.5M |
A day has a fixed cost (closing the bank's books, the snapshot, the unprojected-accounts query) of about 4 ms, so a book of three accounts runs at ~700 acct-days/sec and the rate climbs with the book: ~1,200 at 22 accounts, ~1,700 at 198, and 41.6s for a hundred customers' year against 77s before positions became the truth. The Hetzner rows above are the figure at scale.
HTTP overhead
1 customer, 3 accounts, 60 days, three runs each:
| Mode | Total | us/day |
|---|---|---|
| Direct | 247–256ms | 4,130–4,269 |
| HTTP (httptest POST /advance) | 269–276ms | 4,488–4,615 |
HTTP adds about 7% to a day of three accounts; at any real size it is noise.
Dashboard render
~0.26ms per render (1 customer, 60 days of history), 2,251 allocations.
Bottleneck
Profiled 2026-10-06 (BenchmarkDayScale/pglike/c100/d60, CPU profile):
the pass is 54% of samples and every account's day is a handful of SQL
statements through go-luca:
PositionAtyesterday — one queryApplypostings at a cycle end — a movement insert and a reprojection, monthlyBalance— one queryProjecttoday — a reprojection and a position upsert
SQLLedger.Project alone is 40% of samples (reproject 29%,
upsertPosition 15%). Below it, go-postgres's PG→SQLite translation is
20% (Translate, translateTokens, Tokenize), because every statement
is re-translated at prepare time, and the garbage collector is 32%, the
translation's token slices being the bulk of what it collects.
Optimisation paths
- Cache translated statements in go-postgres — the pass issues the same few statements with different arguments; translating each once would take most of the 20% and much of the GC with it
- Project without re-reading —
Projectre-sums to reproject; the pass already holds yesterday's position and today's balance, so a write-only projection would halve its queries - Real PostgreSQL — bypasses the translation layer entirely; the Hetzner rows above are on it
Notes on memory
The "Allocs" column (allocs/op from go test -benchmem) is cumulative allocations
over the run, not peak usage. Go's GC reclaims most of it.
End-of-Day Processing (legacy results)
| Scenario | Wall Time | Peak RAM | Cumulative Allocs |
|---|---|---|---|
| 1,000 accounts x 3 days | 4.8s | ~145 MB | 3.3 GB / 7.3M allocs |
| 1,000 accounts x 30 days | 47s | - | 28.9 GB / 63M allocs |
| 10,000 accounts x 3 days | 53s | - | 33.3 GB / 73M allocs |