Performance
This page reports the load test run on DigiFIR's development network on 1 November 2026, what it showed about the system's limits, and how the design scales.
WARNING
These figures come from one development machine running every Fabric node, the backend and the test clients together. They describe that setup and its bottlenecks. They are not a capacity figure for production hardware, and no production-scale test has been run yet.
The test
The load test (npm run load:network, scenario S8 of the chaincode specification) files complaints from many citizen apps at once through the real API and measures each filing as its app saw it.
| Setting | Value |
|---|---|
| Clients | 20 citizen apps, each an SDK client with its own device key and a citizen proved through the DigiLocker simulator |
| Filings | 200 in total, one at a time per app: sign, send, wait for the commit and the event, then file the next |
| Stations | Three receiving stations: RJ-JAI-PS07 and RJ-JAI-PS08 (State A), DL-NDL-PS02 (State B), with 70, 70 and 60 filings |
| Production-like work | The worker rendered and registered every filing's receipt, and the hint service scored hints, in the same blocks |
| Host | 4 vCPU Intel Xeon at 2.1 GHz, 15 GB RAM, shared by the network, the backend and the 20 apps |
| Network | Fabric 2.5.16, three Raft orderers cutting a block every 2 s or 10 transactions, six peers with CouchDB, the four chaincodes |
| Backend | One api, one projector and one worker; PostgreSQL 16, Redis 7 and SeaweedFS in Docker |
The run fails unless every filing committed exactly once and reached its app, every receipt was registered once, each station's complaint numbers are consecutive with no gap or repeat, only counter races were invalidated, and no ledger alert was raised.
Results of the final run
| Measure | Result |
|---|---|
| Filings committed | 200 of 200, none lost |
| Throughput over the whole run | 0.90 filings per second (222 s from first send to last commit) |
| Time to commit, p50 / p95 / max | 22.4 / 29.6 / 29.7 s |
| Time to appear in the read views, p50 / p95 / max | 21.1 / 25.9 / 29.2 s |
| Time to a receipt verifiable by its hash, p50 / p95 / max | 24.1 / 29.0 / 32.4 s |
| First submissions refused | 0 |
| Transactions invalidated (counter races, re-endorsed by the backend) | 29 % |
| Blocks; transactions per block | 70 blocks; 8.0 on average, up to 10 |
| Projector, mean time to apply a block | 135 ms |
| Event reaching the app after the commit, p95 | 0.40 s |
Status polls (GET /v1/actions/{id}), p50 / p95 | 8 / 74 ms (run 4) |
| Ledger alerts | 0 |
How the runs improved
The test was run five times while the backend was fixed.
| Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | |
|---|---|---|---|---|---|
| Change | As found | Counter races re-endorsed | Alert only without contention | Submission lanes per station | Receipts registered four at a time |
| Filings committed | 48 of 200 | 200 | 200 | 200 | 200 |
| Filings per second | 0.62 | 0.77 | 0.92 | 0.92 | 0.90 |
| Commit p95 | 10.9 s (the 48) | 66.5 s | 51.9 s | 25.6 s | 29.6 s |
| First submissions refused | 77 % | 35 % | 28 % | 0 | 0 |
| Transactions invalidated | 75 % | 70 % | 67 % | 36 % | 29 % |
| Ledger alerts | 152 | 37 | 0 | 0 | 0 |
What limits throughput
- One filing per station per block. Every filing reads and writes its station's complaint counter, so of two filings of one station in a block only the first commits. The backend now queues a station's filings and re-endorses the loser.
- The block interval sets the per-station ceiling. With a 2 s batch timeout, a block was cut about every 3.1 s, which is about 0.3 filings a second per station. That is over a thousand an hour at one station, far above any real station's rate. Filings at different stations share no key and do not contend, so more stations spread the load instead of adding contention.
- The read side keeps up. The projector applied a block in 108 to 188 ms on average across the runs.
Service level objectives for the pilot
These are targets watched by the monitoring dashboards and alerts. They have not been measured on production hardware.
| Objective | Target |
|---|---|
| Relay, from submit to commit, p95 | Under 4 s |
| Reads, p95 | Under 300 ms |
| Projector lag behind the ledger | Under 5 s |
The project's architecture material expects 150 to 450 transactions a second on production hardware. That figure is an expectation still to be measured.
Scaling the system
| Lever | Effect |
|---|---|
A shorter block interval (BatchTimeout 0.5 s) | Should roughly double each station's rate and halve the queue's wait; an estimate to be measured on production-like hardware |
Larger blocks (MaxMessageCount) | Needed with the shorter interval at higher rates |
| More API replicas | The api is stateless and scales horizontally from 2 to 10 replicas in the Helm chart. Lanes would need to be shared through Redis across replicas. |
| More worker replicas | Receipt registrations share no key, so worker replicas add to their throughput |
| More stations and states | Each station has its own counter and each state its own collections, so they add capacity in parallel |
| Fabric 3.x with SmartBFT | Planned once each institution runs its own orderer; needs no application change |
Network timings
| Operation | Time on the development host |
|---|---|
network.sh up (5 CAs, 3 orderers, 6 peers, 6 CouchDB) | 52 to 61 s |
createChannel | 7 to 10 s |
deployCC (one chaincode) | 25 to 28 s |
| Smoke test (53 checks) | 84 to 90 s |
The whole network from nothing (network.sh all) | About 4.5 minutes |
Deploying the four DigiFIR chaincodes (make deploy-dev) | About 3 minutes |
| Chaincode integration suite | About 22 minutes from nothing |
| Raft leader election after an orderer stops | About 5 s |
Sources: digifir-backend docs/PERFORMANCE.md, docs/architecture/BACKEND.md section 16, docs/PLAN.md (risk P6); digifir-network README.md, docs/MONITORING.md; digifir-chaincode README.md.