Train overhead
A train costs something on every invocation, and the Benchmarks page
has the numbers. Read relative to plain code they look enormous: a Train that adds one to its
input is about 6,900 times slower than input + 1. That comparison says more about input + 1,
which finishes in a fraction of a nanosecond, than about the train.
A fixed cost, not a multiplier
In absolute terms a Train adds about 1.5 μs per invocation, and a ServiceTrain with no effect
providers about 7 μs, most of which is creating the DI scope and running the effect lifecycle
(initialize and save). Each junction adds about 200 ns on top. None of this grows with the work the
junctions do, so the more each junction does, the smaller the share the train takes.
The simulated I/O benchmark shows the effect: three Task.Yield calls bring the base train to about
3.6 times the serial cost, down from 6,900 times. A junction that queries a database (1 to 10 ms) or
calls an HTTP service (50 to 500 ms) puts the train's 1.5 to 7 μs below 0.01% of the run.
What trains are for
Trains are built for business operations where each junction does meaningful work: database queries, API calls, file I/O, domain logic. What they buy at that scale is the railway error handling, the recorded run and the composition, at a cost that is noise next to the I/O. They run comfortably at request granularity, tens to hundreds per second per process.
They are not built for hot paths:
- per-element processing inside a large collection, where a loop or LINQ does the job;
- anything that runs millions of times per second;
- pure computation where each nanosecond counts.
In those places the fixed microseconds dominate, and the run record, which a ServiceTrain writes
when a database data provider is registered, adds a database write per call on top. Keep that work inside
a junction, called once per train, rather than making each element a train.