Hi all,
Canton gets sold into regulated finance, where throughput and latency end up in
contracts. But when an application team asks *“can we settle N transactions a
second at p99 under X ms, and what breaks first?”*, there isn’t really a way to
find out. Generic tools (k6, JMeter, Gatling) can generate HTTP load, but they
have no idea what contention is, or the difference between a transaction that
was rejected and one that was merely slow.
I’ve been building a load/performance harness for Canton applications -
explicitly app-level, driving load at an app’s DARs through the Ledger API. Not
protocol or synchronizer performance; that’s Digital Asset’s domain and I’m
staying well clear of it.
It works, and I’ve used it on real code. Sharing early results because I’d
rather find out now whether this is useful to anyone.
Benchmarking OpenZeppelin’s Canton token template
Measured through the real CIP-0056 interface choice `TransferFactory_Transfer`:
- ~15 transfers/second, p50 ~400 ms, p99 ~1.2 s
- Stops scaling around 22 transfers/s offered
- Their DvP allocation path runs at the same throughput as a plain transfer,
despite doing strictly more work per operation
The interesting part isn’t the rate, it’s what limits it. Every failure
under concurrency is `CONTRACT_NOT_FOUND` - a second transfer reaching an input
holding that another had already archived. The bottleneck is input-holding
selection, not the factory contract. Their implementation archives inputs
first, deliberately, for a contention guarantee; this measures what that costs
when things run in parallel.
For a wallet author that’s actionable: your achievable transfer rate is governed
by your UTXO selection strategy, not by the registry you’re talking to.
One more result that surprised me
Reaching a registry’s factory requires explicit disclosure (the factory is
signed by the admin, so a wallet can’t see it). Attaching that created-event
blob to every submission costs about 3% throughput but raises p99 by ~45%
(803 ms → 1172 ms). The blob is ~576 bytes and rides on every transfer. It shows
up in the tail, not the median.
I haven’t seen that number published anywhere.
Caveats, up front
All of this is a single-participant sandbox on one laptop. They’re floor
numbers. What transfers to a real deployment is the *shape* - where the
bottleneck is and how the system behaves past it - not the absolute rate. Real
figures need a proper deployment, which is exactly what I’d want funding for.
Also: CIP-0104 traffic cost is plumbed and returns the standard’s shape, but a
sandbox has no traffic control, so it reports zero. Cost-per-transaction is
still an open question.
What I’m actually asking
- Does anyone have this problem right now? Specifically: a team with a
launch or an SLA who needs a capacity answer and doesn’t have one. - What would you want measured on your app that isn’t in the list above?
- Is anyone already doing this internally? I’d rather join in than duplicate.
Happy to run it against your app and share the results - the workloads are JSON
files and the reports are self-contained HTML.
Thanks,
dfrnw