Skip to main content
Blnk is designed to process transactions reliably, even when many of them compete for the same balance. Under heavy contention, transactions can be queued and applied asynchronously, allowing the ledger to remain reliable while balances converge to their correct state. This page measures both: normal conditions, where traffic is spread across many balances, and contention, where traffic concentrates on one. These are not peak throughput figures. They show what happens to throughput and processing time as traffic moves from many balances onto one.
This benchmark uses the load testing suite in the Blnk repository, run unmodified. The commands are below, so you can reproduce them against your own deployment.

Setup

We ran these tests on a dedicated VPS running Blnk Core 0.15.4. The server and worker shared one 4 vCPU host; PostgreSQL 16 and Valkey 8 were managed; and k6 ran from a separate 4 vCPU machine. On DigitalOcean, this ledger stack is $230.10/mo at list prices. That figure does not include the load generator. This is the Blnk Core configuration used. Every shape on this page ran against it.

What was measured

Four traffic shapes, from least to most contended. The load test posts every request with inflight: true and skip_queue: false, the suite defaults. The API queues the transaction and returns once it is recorded. A worker then processes it and leaves it inflight. As a result, each shape was measured three times:
  • Accepted throughput. How fast the API takes requests while p95 stays under 300ms.
  • Applied throughput. How fast workers process those queued transactions onto balances.
  • Reliability. Of those accepted requests, how many were processed, and how many got dropped or lost.
Throughput is reported in transactions per minute (TPM). The load test takes its input rates per second, so a RATE of 200 offers 12,000 TPM.

Accepted throughput

To measure how many transactions the API could accept, we increased the request rate in 30-second stages, resetting the ledger between each run. This table shows the highest throughput where p95 latency stayed under 300ms. Contention does not slow down acceptance. The four numbers sit within 6 percent of each other, around 260,000 TPM, or roughly 4,300 requests per second. Accepting a transaction writes a queued record and returns. A worker then picks it up, processes it, and applies it to the balances. Every request in every step returned 201, including the steps past the latency threshold. Nothing failed at any rate, in any shape.

Applied throughput

To measure how quickly workers processed queued transactions, we ran each workload for 30 seconds at its accepted ceiling, then measured how fast the backlog cleared. Result: The API can accept transactions much faster than workers can apply them. After a 30-second burst at the accepted ceiling, the queue took 12 minutes to clear without contention and 37 – 45 minutes when traffic concentrated on a hot balance. Contention reduced applied throughput by roughly 3×. Without contention, workers applied about 9,600 transactions per minute. When many transactions touched the same balance, that fell to between 2,830 and 3,500 TPM. The reason for this gap is serialization. Transactions touching different balances can be applied in parallel. Transactions touching the same balance must wait on the same balance lock, so adding more workers provides less benefit. The type of contention made comparatively little difference. Hot source, hot destination, and hot pair workloads all fell within the same 2,830 to 3,500 TPM range. Hot source was the slowest, about 19% below hot destination. These applied rates are from the configuration above. You can raise them by tuning coalescing, hot-lane routing, and worker concurrency where they apply.

Reliability under contention

To verify that accepted transactions were eventually applied to a balance, we ran each workload for 30 seconds at its measured TPM ceiling, then allowed the worker queue to drain completely. The table compares the number of transactions accepted by the API with the number ultimately processed by the ledger. Any accepted transaction that was never applied would count as lost. Across more than 500,000 transactions, every accepted transaction was eventually processed and applied. No accepted work was dropped, and the queue returned to zero after every run. Contention may delay when a transaction reaches a balance, but it does not cause accepted transactions to be lost.

Conclusion

The practical capacity of a Blnk deployment depends heavily on traffic shape. These results are from a single-server setup: the API and workers shared one 4 vCPU host. On this 4 vCPU production setup, at $230.10/mo, Blnk sustained 9,600 applied transactions per minute when traffic was spread across balances. Sustained over 30 days, that is about 415 million transactions per month. Under heavy contention, where many transactions compete for the same balance, the same setup can still reliably run 2,830 to 3,500 TPM, or about 122 million to 151 million transactions per month.
Note: Horizontal scaling and the right ledger architecture can increase accepted throughput, applied throughput, and monthly capacity across the board.

Reproducing these tests

Run these from the repository root against your own deployment.
1

Run a 30-second load test

Run one workload for 30 seconds at the rate you want to test. Replace the URL with your Blnk server.
Start each run with an empty queue. If transactions from the previous run are still waiting to be applied, they may affect the next result.
2

Repeat for each workload shape

Run the same 30-second test for the remaining shapes by changing the scenario flags.
The suite’s baseline scenario uses a fixed number of virtual users, so it cannot maintain a specific arrival rate. To test uncontended traffic at a known rate, use contention with HOT_RPS=1.HOT_RPS and RANDOM_RPS cannot be set to 0.
3

Wait for all accepted transactions to be applied

After the load test stops, run the following query repeatedly:
When the count stops increasing across two consecutive checks, the workers have finished processing the run.To get applied TPM, pick two readings while the count is still rising. Record the processed count and the clock time for each, then divide the extra processed rows by the minutes between those readings:
For example, if processed goes from 12,000 to 21,600 in 1 minute, applied TPM is 9,600.
Measure against the clock between the checks, not against created_at. That timestamp is when the transaction was created, not when it was applied.
4

Check for lost transactions

Compare the final applied count with the number of transactions accepted during the k6 run.If the two numbers match, every accepted transaction was eventually applied. If applied is lower, some accepted transactions did not reach the ledger.Open the queue monitoring dashboard at http://localhost:5004/monitoring and look for pending, failed, or discarded work to see what happened.

What affects these results

The API server and transaction workers ran on the same 4 vCPU host. Increasing worker concurrency can raise applied TPM because more queued transactions are processed in parallel. But on a shared host, those workers also compete with the API for CPU, which can reduce the rate the API can accept. The load test also creates balances during each run. That adds extra database work to every request. It affects the uncontended workload most, because each transaction creates both a new source and a new destination balance.

Need help?

We are very happy to help you make the most of Blnk, regardless of whether it is your first time or you are switching from another tool. To ask questions or discuss issues, please contact us or join our Discord community.