Scaling & Performance Benchmarks

How the GoBank simulation scales across account counts and what that means for different deployment environments.

Test Environment

CPUIntel Core Ultra 7 165H (22 threads)
OSLinux (amd64)
Go1.25.3
BackendSQLite :memory: (go-sqlite3, pure Go)
Ledgergo-luca double-entry accounting
Date2026-03-17

Headline Throughput

~650

accounts/sec (creation)

~47K

EOD accounts/sec (1K)

~17M

account-days/sec (1K)

Measured at 1K accounts. Throughput degrades at larger scales — see tables below.

Account Creation

Each account creation involves: generate customer, store PII, append to slice, register in go-luca ledger (creates account paths), and fund with initial deposit. This is the most expensive operation per-account due to multiple SQL round-trips through go-luca.

Accounts Wall Time accounts/sec ms/account Cumulative Allocs
1,000 1.5s 675 1.48 803 MB / 1.7M
10,000 16.8s 595 1.68 8.1 GB / 17.5M

Scaling: Near-linear. Throughput drops ~12% from 1K to 10K due to growing ledger size. Each account generates ~8 MB of cumulative allocations (mostly SQL operations in go-luca). The per-account cost is dominated by 3–4 SQL round-trips for ledger registration and initial funding.

End-of-Day Simulation (365 days)

Each simulated day: accrue interest on all accounts (daily_rate = annual_rate / 365), update balances via go-luca, emit transaction log entries. The txLog is cleared each day to bound memory.

Accounts Wall Time (365d) account-days/sec EOD accounts/sec ms per EOD
1,000 22.6ms 17.3M 47,384 0.062
10,000 368ms 10.4M 28,591 1.01

Scaling: Sublinear — throughput drops ~40% from 1K to 10K (10.4M vs 17.3M account-days/sec). The per-day cost grows slightly faster than linearly because go-luca SQL queries (GetAccountByID, BalanceAt, RecordMovement) become slower as the ledger table grows. Memory per iteration is constant (161 KB / 36 allocs) since txLog is cleared daily.

Full Year (Create + Simulate)

Combined benchmark: create all accounts then simulate 365 days.

Accounts Total Create Simulate Create %
1,000 1.9s 1.5s 22ms 99%
10,000 17.5s 17.0s 417ms 97%

Key insight: Account creation dominates total runtime (~97–99%). Once accounts exist, simulating an entire year is fast. This means the simulation is well-suited for long-running scenarios where you create once and simulate over extended periods.

Extrapolated Scaling

Projections based on observed scaling behaviour. Actual numbers may differ — the sublinear degradation in SQL performance means these are lower bounds for time.

Accounts Create (est.) Sim Year (est.) Total (est.) Cumul. Allocs (est.)
1,000 1.5s 23ms ~2s ~1 GB
10,000 17s 370ms ~17s ~8 GB
100,000 ~3 min ~6s ~3 min ~80 GB
1,000,000 ~30 min ~2 min ~30 min ~800 GB

Note: “Cumulative Allocs” is total bytes allocated and freed over the run (from -benchmem), not peak RSS. Go’s GC reclaims most of it. Observed peak RSS for 1K accounts is ~145 MB.

Environment Sizing Guide

Each environment has a natural scale determined by available RAM (for the in-memory SQLite backend) and acceptable wall-clock time for initial creation.

Environment RAM Natural Scale Create Time Year Sim Use Case
Raspberry Pi / gokrazy 2–4 GB 1K–5K < 10s < 1s Demos, IoT dashboard, teaching
Small VPS 4–8 GB 5K–20K < 1 min < 5s Dev/test, CI benchmarks
Laptop / Workstation 16–64 GB 10K–100K < 3 min < 10s Development, scenario testing
Cloud instance 64–256 GB 100K–500K < 15 min < 1 min Stress testing, regulatory scenarios
High-memory server 256 GB+ 1M+ < 30 min < 5 min Full-scale retail bank simulation

WASM (browser): Limited to browser memory (~2–4 GB). Safe ceiling is ~1K–5K accounts. The demo defaults to 1K for this reason.

Resource Constraints

ConstraintLimitNotes
Benchmark timeout 10 min (quick), 1h / 24h (full) Set via -timeout flag in Taskfile. Quick bench caps at 600s to keep CI fast.
Memory (SQLite :memory:) Available RAM The in-memory database grows with account count. No disk I/O, but no persistence either. Peak RSS is much less than cumulative allocs due to GC.
Disk (temp files) Minimal SQLite :memory: writes nothing to disk. The Go test binary and any core dumps are the only disk use. No 100 GB temp file risk with current backend.
CPU Single-threaded Current simulation is single-goroutine under a mutex. Multi-core machines get no scaling benefit. This is the most obvious optimisation path after batch SQL.

Bottleneck Analysis

The dominant cost is go-luca’s per-account SQL round-trips, each requiring:

  1. GetAccountByID — fetch account details
  2. BalanceAt — query closing balance
  3. RecordMovement — insert interest movement

That’s 3 SQL operations per account per day for interest accrual alone. Account creation adds further round-trips for ledger registration and initial funding.

Optimisation Paths (not yet implemented)

OptimisationExpected ImpactComplexity
Batch SQL in go-luca (bulk balance query + bulk insert) 10–100x for EOD processing Medium — requires go-luca API changes
Parallel daily processing (shard accounts across goroutines) Near-linear with core count Medium — requires ledger to support concurrent writes
File-backed SQLite (avoid RAM ceiling) Unlocks 1M+ on modest hardware, slower per-op Low — change DSN from :memory: to file path
PostgreSQL / CockroachDB backend Production-grade scaling, persistence High — requires go-luca backend abstraction

Sense Checks

  • Linear creation: 1.48ms/account at 1K vs 1.68ms at 10K — ~12% degradation is consistent with B-tree index growth in SQLite. Plausible.
  • Sublinear simulation: 10x accounts takes 16x as long — consistent with SQL query time growing with table size. The O(n log n) profile matches SQLite B-tree lookup cost increasing as the movements table grows.
  • Memory constant per-day: 161 KB / 36 allocs per simulation iteration regardless of account count — confirms txLog clearing works and daily overhead is fixed.
  • Creation dominates: 97–99% of total time is account creation. This makes sense: creation does ~4 SQL round-trips per account versus simulation doing 3 per account per day, but creation also involves RNG, PII storage, and slice management.
  • Cumulative vs peak: 8.1 GB cumulative allocs for 10K accounts, but peak RSS is far lower thanks to GC. The B/op metric from -benchmem is total allocated, not retained.

Running Benchmarks

All benchmarks are defined in cmd/demo/benchmark_test.go and invoked via Taskfile:

CommandScopeTimeoutTypical Duration
task benchFull year, 1K–10K10 min~20s
task bench:createAccount creation, all sizes1h~20 min
task bench:simYear simulation, all sizes24hvaries
task bench:fullFull year, all sizes24hvaries
task bench:allEverything24hvaries

Custom metrics reported: accounts/sec, account-days/sec, eod-accounts/sec, create-ms, sim-ms.