Live set load run
The Live Hub promises that a completed set is on the board at once. Whether that holds when a whole group lifts cannot be read off the code, and the server's cost for one set had not been measured. The load run measures it: up to twenty simulated lifters complete sets into one live session, and for every set the run records how long it takes from the request to the set's appearance on the session's set stream — the stream a wall display follows.
The run is tests/live/load/live-set-load.ts, started through the live test
tests/live/offline/17-live-set-load.live.test.ts. It talks to backdev only,
with one disposable owner that is deleted again.
Running it
ENODE_LIVE_LOAD=1 npx vitest run --config tests/live/vitest.config.ts \
tests/live/offline/17-live-set-load
Without ENODE_LIVE_LOAD=1 the test is skipped: it stores a few hundred sets
on backdev and takes about five minutes, so it does not ride along with the
rest of the live suite. The report is printed and written to
tests/live/out/live-set-load-<run>.txt; every set's record is in the .json
beside it.
The profile is set with environment variables:
| Variable | Default | Meaning |
|---|---|---|
ENODE_LOAD_LIFTERS | 1,2,5,10,20 | How many lifters send at the same time, one step after the other. |
ENODE_LOAD_SETS | 6 | Sets every lifter completes in a step. |
ENODE_LOAD_REST_MS | 3000 | A lifter's rest between two sets, varied by a quarter either way. |
ENODE_LOAD_BURSTS | 3 | Rounds, after the steps, in which every lifter completes a set in the same instant. 0 leaves the step out. |
ENODE_LOAD_REPS | 5 | Repetitions per set. Each is two reps on the wire, down and up, with one sensor package each. |
ENODE_LOAD_STANDINGS | on | 0 leaves out the standings reads a tablet makes after each set it delivered. |
The run loads a server other people use. On 2026-10-07 backdev stopped
answering for about a minute (requests timed out, then 502) right after the
third run of that morning. That run had cleaned up by deleting its twenty
athletes five at a time, and nine of those deletes ran into the 15 s deadline
of a request. The likely cause is that cleanup and not the sets — the two runs
before it, which deleted one athlete after the other, had no such effect — but
only the server's log for 07:46 to 07:49 UTC can say. The cleanup deletes one
athlete at a time since. Run the full profile when nobody else is testing
against backdev, and not several times in a row.
What is real and what is simulated
- Real: the wire. A set is built by the app's own mapper
(
toLiveSessionSetCreateDto) with reps and gzipped sensor packages, and sent to the route the app sends to, from a tablet with a device session of its own. Every lifter has a tablet. - Real: what a tablet does after a set, where the server does not push to
it. Such a tablet re-reads the session's standings once the set is
delivered (
packages/core/src/live-session/standings-follow.ts), paced by the app's own scheduler (read-scheduler.ts): one read on its way, one more remembered, two seconds apart. The run uses that scheduler, because those reads are part of what a set costs. Until 2026-10-07 a tablet read when the set was queued and again when it was delivered, and dropped the second read while the first was on its way; the first runs below measured that. - Not in the run: a tablet the server pushes to. A tablet signed in as the session's owner follows the set topic, reads once and nothing per set (backdev, 2026-10-07). The run measures the costlier way, which is what an athlete's own phone does today.
- Simulated: the pace. A lifter in a gym completes a set every two to three minutes. In the run a lifter completes one every three seconds, so twenty lifters send like several hundred. The steps therefore show where the path gives way, not what a training looks like.
- Realistic: the last step. Every lifter completes a set in the same instant — a group that lifts on a signal.
- Not in the run: the live outbox. The outbox is one queue per device, and
one Node process has one. It is checked by
12-live-outboxand by the self-test's Live chapter.
Reading the report
One line per step:
| Column | Meaning |
|---|---|
sets/s | Sets per second the step sent, over its whole length. |
answer ms | From sending the request to having read the server's answer. |
on the stream ms | From sending the request to the set's live_session_set.created frame on the set stream. This is what a wall waits for. |
standings read ms, kB | One standings read of a tablet (GET /workout_live_session_sets/reduced/:id), and the size of the largest answer in the step. |
stale | Sets after which the tablet's standings do not show the set it has just completed: the read that followed the delivery failed, or came back without the set. |
failed | Creates that were not answered 201. |
lost, twice | Sets the session does not hold after the step, or holds more than once. |
not shown, shown twice | Stored sets with no frame on the stream, or with more than one. |
p50 is the middle value, p95 the value 95 of 100 sets stay under. Under the
table the report prints the time to the stream for every single set, in the
order the sets were sent.
The test fails when a step has a value other than 0 in failed, lost,
twice, not shown or shown twice. The times are a measurement, not a
limit; the test asserts none.
What the first runs showed
Four runs against backdev on 2026-10-07 between 09:30 and 10:00, 912 sets in all. Each is one run on a server that other tests used at the same time, so the times are an order of magnitude, not a benchmark.
No set was lost, stored twice, missing from the stream or on it twice, in any step of any run.
A set of five repetitions (ten reps with a package each) is 32 kB on the wire. A request the server has nothing to do for took about 0.1 s from the same machine.
Full profile, standings reads on (09:33):
| Lifters | Sets/s | On the stream, p50 / p95 ms | Standings read, p50 / p95 ms | Largest read |
|---|---|---|---|---|
| 1 | 0.29 | 841 / 921 | 242 / 370 | 14 kB |
| 2 | 0.48 | 608 / 815 | 288 / 499 | 42 kB |
| 5 | 1.30 | 706 / 891 | 567 / 890 | 110 kB |
| 10 | 2.42 | 740 / 997 | 1129 / 1637 | 251 kB |
| 20 | 2.02 | 1818 / 2928 | 6518 / 10162 | 531 kB |
| 20 at once | 1.25 | 5465 / 7250 | 12454 / 14926 | 653 kB |
Full profile, standings reads off (09:39):
| Lifters | Sets/s | On the stream, p50 / p95 ms |
|---|---|---|
| 1 | 0.26 | 1031 / 1394 |
| 2 | 0.50 | 904 / 1302 |
| 5 | 1.25 | 653 / 1138 |
| 10 | 2.63 | 626 / 938 |
| 20 | 5.33 | 651 / 1135 |
| 20 at once | 3.82 | 2797 / 3168 |
What follows from them:
- One set costs the server about half a second. A set alone was on the stream after 0.4 to 1.4 s, and after 0.5 to 0.7 s in the middle once a run had warmed up; the first step of every run is its slowest, with six sets to go by. The frame arrives within some tens of milliseconds of the answer, so the stream adds next to nothing: the time is the server storing the set.
- The set path itself does not grow up to twenty lifters. Without the standings reads the time to the stream stayed at about 0.65 s from five lifters to twenty, at 5.3 sets per second.
- Everybody at once takes seconds. Twenty sets in the same instant were on the stream after 2.8 s in the middle and 3.6 s at the latest, without the standings reads.
- The cost of a set depends on its reps. With one repetition per set (two reps, 7 kB) the time at ten and twenty lifters was 0.32 to 0.40 s, against 0.63 to 0.65 s with five (run of 09:44).
- The standings read is what grows. One set weighs 2.4 kB in that read, whatever its reps: 783 bytes are the athlete's profile and 354 bytes per measurement are the metric's definition, both repeated for every set. The read therefore grows with the session — 653 kB at 288 sets — and every tablet sends it up to twice per set. With the reads on, the same ladder got to 2 sets per second instead of 5.3, the time to the stream at twenty lifters was 1.8 s instead of 0.65 s, and a read took 6.5 s in the middle. In the last step reads took up to 15.2 s, which is past the app's deadline for a request (15 s).
- A slow standings read leaves the tablet's standings without the set just lifted. The read sent when the set is queued reaches the server before the set does. When it is still on its way at the moment the set is delivered, the second read is not sent, and the standings stay as they were until the next set. In the run of 09:55 (ten lifters, 189 kB per read) that happened for 3 of 50 sets, and each time the standings lacked the set. In the run of 09:33 the second read was not sent for any set from ten lifters on.
The two runs with standings reads differ at ten lifters: a read took 1.1 s in the middle at 09:33 and 0.2 s at 09:55, for a session of about the same size. Either something else used the server at 09:33, or the reads slow each other down more than their size explains. One more run on a quiet server would tell.
The last two points are not in the load run's own files. They are passed on in the Live Hub page, and closed by block W9; see the next section.
After the tablet reads by the new rule (2026-10-07, 13:09 and 13:12)
Block W9 changed when a tablet reads its standings, and the run reads the same
way since. Two runs on a backdev that seven other sessions were testing
against at the same time: the profile of the run of 09:55
(ENODE_LOAD_LIFTERS=1,5,10 ENODE_LOAD_SETS=5 ENODE_LOAD_BURSTS=1) at 13:12,
and its ten-lifter step alone at 13:09.
| Run | Lifters | Sets | Sets/s | On the stream, p50 / p95 ms | Standings read, p50 / p95 / max ms | Largest read | Stale |
|---|---|---|---|---|---|---|---|
| 09:55, the old rule | 10 | 50 | 2.79 | 555 / 723 | 207 / 396 / 471 | 189 kB | 3 |
| 13:12, the new rule | 1 | 5 | 0.26 | 723 / 878 | 242 / 325 / 325 | 12 kB | 0 |
| 5 | 25 | 1.29 | 543 / 791 | 140 / 339 / 344 | 71 kB | 0 | |
| 10 | 50 | 2.78 | 494 / 758 | 174 / 370 / 410 | 189 kB | 0 | |
| 10 at once | 10 | 5.84 | 1129 / 1264 | 415 / 496 / 496 | 213 kB | 0 | |
| 13:09, the new rule | 10 | 50 | 2.56 | 517 / 800 | 165 / 370 / 528 | 118 kB | 0 |
- No set is missing from the standings of the tablet that lifted it: 0 of 140 in the two runs, against 3 of 50 in the ten-lifter step of 09:55. Every set was in the read that followed its delivery, also with ten sets in the same instant.
- One read per delivered set, where there were up to two. A read still returns the whole session, so its size and its time at a given size are what they were.
- No set was lost, stored twice, missing from the stream or on it twice.
- Not run again: the ladder to twenty lifters. The growth of a read with the session is unchanged, so the numbers of 09:33 for twenty lifters stand for a device that reads, until the server sends less per set (handoff S2 and item 5) or admits every linked device to the set topic (P1). A tablet of the session's owner does not read per set at all; the run does not measure it.