Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
495 changes: 492 additions & 3 deletions CHANGELOG.md

Large diffs are not rendered by default.

29 changes: 23 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,29 @@ trusted publishing, provenance attached; 1.0.0 was the manual bootstrap. Semver
applies — a breaking change to a documented API needs a major, and the eight primitive shapes, the
`x` CLI surface and the tier table are as stable as the `X_*` codes already were.

Realtime capacity is **measured once, on one node**: 50,000 real WebSocket clients against a single
`sync` node over `InProcessTransport`, `SIGKILL`ed with no drain. All 50,000 reconnected; **49,981**
received a channel patch inside the window. Time-to-consistent p50 54.0s / p90 105.5s / max 145.7s;
156,851 connect attempts shed by the `AcceptBudget` before any query or snapshot path. Per-node
recovery — the run never crossed NATS, so it is **not** a multi-node result and not a throughput
figure. [`scripts/bench/restart-bench.ts`](scripts/bench/restart-bench.ts), result committed under
Realtime capacity is **measured on one node, in two halves that answer different questions**.

**Reachability, 50,000 clients:** real WebSocket clients against a single `sync` node over
`InProcessTransport`, `SIGKILL`ed with no drain. All 50,000 reconnected; **49,981** received a
channel patch inside the window, p50 54.0s / p90 105.5s / max 145.7s; 156,851 connect attempts shed
by the `AcceptBudget` before any query or snapshot path. That figure times **first delivery on the
reconnected socket** — reconnect *and* resubscribe *and* one patch. It was labelled
"time-to-consistent" until 2026-08 and never measured consistency: the harness recorded
`lastSeenSeq` and read it nowhere, so a patch the node dropped was invisible to it by construction.
The timings are unchanged and still stand; only the name was wrong.

**Delivery, 10,000 clients:** the same harness, now counting holes in the probe sequence each client
receives, per connection ([`scripts/bench/restart-bench-seq.ts`](scripts/bench/restart-bench-seq.ts)).
10,000 clients, a probe every 200ms, all 10,000 reconnected: **1,666,882 patches received, 0 lost** —
no gap, no duplicate, no rewind on any client. It needs its own counter because a channel topic has
no cursor and no re-snapshot: a frame `SyncSocket.send` drops under backpressure is unrecoverable,
and `SocketRegistry.deliver` and `ChannelHub`'s bridge both discard the `false` that would have said
so. `As of 2026-08` this is the only run with delivery accounting — the 50,000-client result predates
the counter and carries no delivery number at all.

Per-node recovery in both: neither run crossed NATS and neither subscribes to a live query, so no
cursor, snapshot or gap-repair path is under test. Not a multi-node result and not a throughput
figure. [`scripts/bench/restart-bench.ts`](scripts/bench/restart-bench.ts), results committed under
[`scripts/bench/results/`](scripts/bench/results/).

Open: roadmap milestone 11's two-platform deploy proof — 1.1.0 gave a scaffolded app a real
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ are the difference between an agent that ships and one that thrashes.

| What was measured | The result |
|---|---|
| **Realtime restart recovery** | 50,000 real WebSocket clients against **one** `sync` node, `SIGKILL`ed with no drain — no `reconnect` frame sent, so every client recovers on its own backoff. All 50,000 reconnected; **49,981** received a channel patch inside the window. Time-to-consistent p50 **54.0s**, p90 **105.5s**, max 145.7s |
| **Realtime reachability** | 50,000 real WebSocket clients against **one** `sync` node, `SIGKILL`ed with no drain — no `reconnect` frame sent, so every client recovers on its own backoff. All 50,000 reconnected; **49,981** received a channel patch inside the window. Time to first patch on the reconnected socket: p50 **54.0s**, p90 **105.5s**, max 145.7s. It times reconnect + resubscribe + one delivery, and nothing after it — it was called "time-to-consistent" until 2026-08, and it could not see a lost patch |
| **Realtime delivery** | 10,000 clients, same forced restart, a probe every 200ms: **1,666,882 channel patches received, 0 lost** — every client's sequence dense across its connection, no gap, no duplicate, no rewind. `As of 2026-08` the only run with delivery accounting; the 50,000-client result predates the counter and has none |
| **The DB-load half** | **156,851** connect attempts shed by the shipped `AcceptBudget` (500/s, burst 2000) before reaching any query or snapshot path. Recovery is bounded by admission control, not by the matcher |
| **What it is not** | one node, in-process transport — the run never crossed NATS. Per-node recovery, **not** a multi-node result, and not a throughput or latency-under-load figure |
| **What it is not** | one node, in-process transport — neither run crossed NATS, and neither subscribes to a live query, so no cursor, snapshot or gap-repair path is under test. Per-node recovery, **not** a multi-node result, and not a throughput or latency-under-load figure |

Reproduce it: `bun run scripts/bench/restart-bench.ts --clients 50000` — the committed report and the run's own transcript are in [`scripts/bench/results/`](scripts/bench/results/).
Reproduce it: `bun run scripts/bench/restart-bench.ts --clients 10000 --probe-interval-ms 200` — the committed report and the run's own transcript are in [`scripts/bench/results/`](scripts/bench/results/).

**Not claimed at 1.1.0:**

Expand Down Expand Up @@ -244,7 +245,7 @@ The same app code on one PaaS dyno and on a replicated cluster. Climbing is a dr
| 3 | Kubernetes, per-role HPAs, logical replication for the change feed | none, plus config |
| 4 | distributed SQL (YugabyteDB), JetStream R3, metrics and traces wired end to end | none for the datastore swap — with named incompatibilities |

**This is the design, not a demonstration.** `As of 2026-08` exactly one point on it is measured — the 50k restart above, at one node. Rung 4 has never been run. [`17-scale-ladder.md`](docs/idea/17-scale-ladder.md) states rung by rung what is real today and what is intent, and names the places the invariant currently breaks. Fintech, agent platforms, multi-tenant dashboards: that is what the architecture is *for*, and nobody's production traffic has tested the claim yet.
**This is the design, not a demonstration.** `As of 2026-08` exactly one rung is measured — rung 1, the single node, by the two runs above: 50k for reachability, 10k for delivery. Rung 4 has never been run. [`17-scale-ladder.md`](docs/idea/17-scale-ladder.md) states rung by rung what is real today and what is intent, and names the places the invariant currently breaks. Fintech, agent platforms, multi-tenant dashboards: that is what the architecture is *for*, and nobody's production traffic has tested the claim yet.

→ [The scale ladder](docs/idea/17-scale-ladder.md) · [Running it for real](docs/ops/README.md) · [Mobile and desktop targets](docs/idea/16-app-targets.md)

Expand Down
Loading