Skip to content

[Meta] Production-readiness review 2026-08-05 — verified findings catalogue #913

Description

@pathosDev

A ten-lens production-readiness review of v0.13.0, run against the code rather than against the tracker, and re-verified before filing.

121 findings carried into verification → 7 folded into existing issues → 114 filed (29 blocker, 38 high, 39 medium, 8 low). 67 were reproduced by running code; 39 were confirmed by reading the cited lines where a live cluster or a real broker would have been required. A further 172 pre-existing issues were labelled production-goal rather than re-filed.

The question the review asked was narrow: ignoring the README disclaimer, what is the actual technical state, and what stands between this codebase and running it for real?

Why this batch is verified rather than reported

The review's most consequential finding is not any single defect — it is a pattern that makes ordinary code review unreliable here. Comments and JSDoc in this codebase systematically assert invariants the code does not implement. Not as sloppiness; as a consistent shape:

The comment says The code does
Cluster.ts:804 — "FD-driven downing is the advisory fallback when no DowningProvider is configured" deletes the member unconditionally; there is no if (!this.downing) (#929)
KeepOldest.ts:52-57 — "the conservative default is still: keep the side with the oldest" both branches return the identical set, so downIfAlone is dead (#932)
ActorCell.ts:191 — "Unbounded … is still available via withMailbox(() => new Mailbox())" Mailbox is exported from no entry point and there is no subpath (#661)
PersistentActor.ts:261 — "Persist several events atomically" D1 has no transaction at all (#959)
ReplicatedEventSourcedActor.ts:486 — "read highestSeq + append in one mailbox tick" sits directly above a void this._appendOne(...) (#958)
LWWRegister.ts:60 — "break by replica id so every node converges to the same winner regardless of arrival order" returns this when the replica also ties, so merge is order-dependent (#950)
MailboxVariants.test.ts:249 — "assert behaviorally: fill past capacity, observe drop-head" expect(droppedCount).toBe(0) on a mailbox that received nothing (#1020)
BasicAuth.ts:48 — "a miss still burns the same comparisons" && short-circuits, so the password compare runs only for a known user (#968)
ShardedDaemonProcess — "each daemon … its own shard via a 1-to-1 allocation" ids are hashed; 100 daemons land on 58 shards (#951)

Because of that, every finding was re-checked against the source before it became an issue, preferentially by execution rather than by reading. Each child issue carries a Verification status section stating which of the two it got. That discipline changed the outcome in both directions — see Reviewed and not pursued below.

Ground rules applied to every child

  • Verified before filed. CONFIRMED means reproduced by running it; CONFIRMED-BY-READ means the mechanism is unambiguous at the cited lines but not runtime-observable here. Both are stated explicitly; neither is implied.
  • Cite the line, not the comment. Every evidence block opens with path:from-to and quotes the current tree. Where a comment or a doc page contradicts the code, both are quoted.
  • No duplicates. Findings that already had an issue were not re-filed — the existing issue was labelled and, where this review added evidence it did not already carry, commented on.
  • production-goal is a gate, not a batch marker. It sits on 287 of the 668 open issues, including ones filed months before this review. Filtering on it answers "what is still between us and production", not "what did this review find".

Children

Core runtime, typed API and patterns (15)

Cluster membership, transport, downing and leases (19)

Sharding, singleton, reliable delivery and CRDTs (10)

Persistence (7)

Security and serialization (8)

The I/O edge — HTTP, WebSocket, brokers (17)

Performance and benchmark validity (4)

Operability (9)

Packaging, public API and release engineering (9)

Verification rigor (10)

Docs-vs-code drift (6)

Reviewed and not pursued

Seven findings did not become issues. Each was folded into an existing one, with the new evidence added there as a comment:

Nine further findings were narrowed rather than dropped, and their issues say so rather than overstating the claim. The notable ones:

Two claims came back stronger than reported: the terminating-actor busy-spin measured 123 683 dispatcher executions in 300 ms rather than "a hot loop" (#915), and Bun's All files coverage row was shown to be an unweighted mean over files, not a weighted line ratio (#1016).

Working order

Sequenced so the cheapest total-outage fixes land first.

  1. Four verified one-line blockers. [Bug] ensureCoordinator runs before numShardsByType is populated, so every ShardCoordinator is built with 64 shards whatever you configure and entities above that id never get a home #1026 (ensureCoordinator reading options.numShards before the map is populated — every coordinator is built with 64 shards whatever you configure), [Bug] The failure detector deletes a peer at downAfterMs regardless of the configured DowningProvider, so every split-brain resolver is bypassed under a comment claiming it is only a fallback #929 (the failure-detector delete behind an if (!this.downing)), [Security] Lease.checkAlive() returns a cached boolean instead of comparing against expiresAt, and has no callers, so two nodes can both believe they hold the lease after an event-loop stall #937 (checkAlive() compared against expiresAt instead of returning a cached boolean), [Bug] An actor that throws in onReceive is restarted with no log output at any level, so a crash-looping actor is invisible in production #916 (failToParent logging the cause — an actor that crash-loops currently produces zero log output at any level). Four small changes, four total-outage classes.
  2. [Bug] ask() rebuilds the message as an object literal, so Map, Set, Date, typed arrays and every class instance arrive stripped of their contents #925ask(). It rebuilds the message as an object literal, so Map, Set, Date, typed arrays and every class instance arrive stripped. Silent data loss on the most-used API in the documentation.
  3. The wire-identity class as one fix. [Security] Forged heartbeat.from keeps a dead node "healthy" forever (blocks singleton/shard failover) and makes the node dial an attacker-chosen host:port #572, [Security] Receptionist gossip trusts the payload's self-declared from instead of the connection-authenticated sender, letting any peer poison cluster-wide service discovery #574, [Security] DistributedPubSubMediator.handleGossip trusts message.from instead of the socket peer, letting one peer wipe another node's subscriptions #582, [Security] ShardCoordinator derives region identity and shard ownership from node/region/hostedShards in the payload rather than the authenticated envelope sender, letting one peer seize every shard of a type or evict another node's region #712, [Security] DistributedData counts quorum acks and read-responses by the payload's from instead of the authenticated peer, letting one member forge a full quorum and inject arbitrary CRDT state #719, [Security] onReadRequest/onWriteRequest reply to the payload's from, so any node can be made to dial an attacker-named host and buffer full CRDT snapshots in a Connection.pending queue that is never drained, never capped and never reclaimed #723 and [Security] decodeSingleRef builds a dialable NodeAddress from unvalidated wire fields, so a ref embedded in any message body makes the receiving node connect to an attacker-chosen host #936 are the same defect at seven call sites: a wire-supplied address trusted instead of the authenticated peer. One helper that resolves any such address against the connection retires the class; [Security] The cluster hello identity is not bound to the TLS peer certificate, so mTLS admits a node but never verifies which node it is #912 is the foundation it rests on.
  4. The three inverted delivery guarantees. [Bug] KafkaActor resolves its eachMessage promise before the handler runs while autoCommit is on, so the documented at-least-once default is at-most-once, and a partial withConsumer erases a HOCON manual commit mode #975 (Kafka), [Bug] AmqpActor acks each delivery before handing it to the target actor and swallows a failing ack, so the default configuration loses messages on restart and redelivers duplicates when the ack fails #976 (AMQP), [Bug] MQTT QoS 1 and 2 are structurally unreachable because MqttActor never enables manualAcks, so mqtt.js PUBACKs while the message is still in the mailbox and MqttMessage.qos reports a guarantee the actor cannot provide #977 (MQTT) all acknowledge before the handler runs, so the documented at-least-once default is at-most-once on every broker that claims it.
  5. Two CI gates that prevent recurrence. [Feature] No file in the repository imports actor-ts by its published name and doc fences are never type-checked, so the exports map has no end-to-end test and publint plus attw stay green while five subpaths are unresolvable #1003 (a consumer smoke gate — npm pack, install, resolve every documented import) would have caught [Docs] 92 documented imports use the subpaths actor-ts/http, /coordination, /serialization, /discovery and /cluster/pubsub which are absent from the exports map, so every HTTP documentation page fails with ERR_PACKAGE_PATH_NOT_EXPORTED #1001's 92 broken doc imports on the day they were written; [Test] Bun reports the All-files coverage row as an unweighted mean over files and bunfig.toml never sets coverageSkipTestFiles, so 319 test files sit in the badge at 100 percent and the real src figure is about 5 points lower #1016 (coverageSkipTestFiles) is why the coverage number is not what it appears to be.
  6. [Bug] Gossip and heartbeats target only reachable members, so a symmetric partition never heals and both halves run as independent clusters until a process restart #930 — partitions never heal. Gossip and heartbeats target only reachable members, so a symmetric partition forks permanently. Every split-brain resolver above is only meaningful once the losing side can rejoin.

Then the remaining waves by severity. The production-goal label is the working set; severity: high plus priority: high within it is the head.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: highTop priority — high impact, plan nextproduction-goalBlocks or defines the path to production readiness

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions