Limits of the current design and implementation, not open questions about the invariants. Key words are as defined in §1.2 [RFC2119] [RFC8174].
Current implementation clauses: October 12, 2026
Capacity
The roughly 56-node arithmetic belongs to the broker-era tunnel allocator. A designed replacement is not a measured region-size ceiling. The reviewed FD&F records do not establish a maximum region population.
Packet aliases
Aliases translate IPv4 packet addresses and checksums, including quoted ICMP packets. Application payload addresses need their own handling. IPv6 is not established by these IPv4 tests.
Scopes and policy
Several overlapping ponds are qualified in namespaces. Arbitrary recursive depth and automatic transitive authorization are not established. Resolution refuses when several ponds answer.
Recovery and installation
Memory replacement is not durable replication. Fresh region installs without broker/key dependencies remain cleanup work. Python reset integration, some monitor presentation and hardware failover timing remain open.
The detailed clauses, rationale, source citations and author commentary below remain the mechanism reference. Where the added region or modern Ribbit contract differs, use the implementation-specific scope stated above; the local and broker paths remain separately identified.
FrogNet Specification — Draft 0.9, revision 2026-08-29. Cite a conformance claim against this revision, not against a section number alone.
These are limits of the current design and implementation. They are not open questions about whether the invariants in §2 hold. None is hidden behind a fallback: each fails visibly and each carries a status — structural, implementation limitation, or unresolved. A structural limit follows from an invariant in §2 and cannot be removed without removing the guarantee that produces it. An implementation limitation is a property of this build. An unresolved item is one the specification does not yet determine.
An implementation claiming conformance MUST reproduce these limits or state explicitly which it has removed and how, since several of them are consequences of the invariants rather than defects to be repaired.
2. Observation latency is the reader's, and is bounded by its poll interval
This is not the absence of a fabric feature. Earlier revisions listed “no change notification” here as a missing capability. That framing was wrong and is withdrawn: how an application observes change is application policy, and a fabric that knew which changes mattered to which application would have application semantics coupled back into it — the coupling this architecture exists to remove (§10.3a).
Consequence. An application that must notice a change chooses a mechanism and pays for it. Observation latency is then bounded by the interval that application chose, not by anything FrogNet does. Polling is inexpensive here because an unchanged read collapses to a SAME frame (§11), but inexpensive is not free, and a reader that polls slowly observes late. That cost is real and it belongs in this section; the boundary that produces it does not.
3. One serialization point is a write ceiling
Mechanism. Every write in a connected pond is ordered at the elected database host (§2b).
Consequence. Writes are bounded by the throughput of one point and do not scale horizontally. The coherence property and the scaling limit are the same fact stated twice, so this limit cannot be removed without removing the guarantee that makes the model work. What it does not mean is that the ceiling is fixed. Every term in the database-host score is a static property of the hardware or its configuration [DBHOST_STATIC_RANK_V1], so an operator who provisions a machine built for the role — memory, storage class, CPU — gets that machine elected whenever it is visible, deterministically and without configuring anything. The bound is the capacity of the machine placed under the role, not the capacity of the weakest node that happens to be present. A pond with one large host and forty small ones is an ordinary shape, and the ceiling moves with the large host.
3.1 Fronting an external database is not supported today
The obvious extension is to elect a node that fronts a database rather than running one — a modest machine holding the role while the storage sits in a managed service behind it. The rest of the pond would be unaffected: it resolves the role by name and does not know what backs it.
The election machinery does not permit this at present, and the obstacles are specific.
The eligibility gate is local.mysql_running is set by pgrep -x mysqld on the candidate itself. A node fronting a remote service has no local process to detect and is gated out before it is scored.
The score measures the wrong machine. Every term — installed RAM, innodb pool, disk class, cores × clock — is a property of the candidate, not of the store it would reach. A small fronting node backed by a large service would lose to a large local node backed by nothing.
Neither is a deep obstacle: the connection host is already configurable (DB_CONFIG.json), so the data path needs nothing. What is missing is a way for a candidate to advertise the capacity of a store it reaches rather than the hardware it runs on, and [DBHOST_STATIC_RANK_V1] constrains the answer — whatever is advertised MUST be static, or holding the role could change the score that won it.
Recorded here as an unbuilt capability rather than a claim. An implementation MUST NOT report an external store’s capacity through the existing capability fields, which are specified as properties of the candidate machine (§8.4).
Contention on a single key is whatever the backing database does about contention on a key; a thousand writers fighting over one value is a design problem no substrate repairs. In the FrogNet programming model this is a signal to stop sharing the value, not to engineer increasingly elaborate contention management. Give each independent producer its own tuple and let consumers operate across those values with get_all (§2b, §10.3b).
Reads are not bounded the same way, and not by replication. Nothing is replicated (§2a). Reads relieve the store by three mechanisms, each of which preserves the single copy:
Mechanism
Where
What constrains it
Receive-side collapse
Any node answering
N callers asking the same question in the same instant cost one origin execution among them (§11.12)
Proven-current RAM materialization
Database host only
Served only where a generation counter proves RAM equals disk on this request; fails closed [DATA_CACHE_GEN_V2]
Local read cache
Every node, its own forwards only
Scoped invalidation plus a short TTL; bounded-stale within the freshness window, which the store evaluates on the clock that stamped the row (§9.4)
None of these is a second authoritative copy, and an implementation MUST NOT describe them as replication.
Read volume is also not read cost. The naive reading of one store serving many nodes is many connections, many requests, and a full value returned each time. Two properties of the transport make that the wrong model. Sockets between a proxy and its daemon are persistent (§11a), so a read is not a connection setup. And an unchanged answer is a twenty-one byte frame regardless of the size of the value (§11.10), so a consumer asking a question whose answer has not moved pays a constant, not a payload.
The conventional cost is consumers × frequency × response size. Here the steady state is consumers × frequency × a constant, with the payload transmitted when the answer actually changes. A high read rate against a stable value is therefore cheap on the wire, and a rising request count does not imply a proportional rise in network load (§14.1b).
The claim is not that the limit disappears. It is that the limiting resource changes. Every machine is bounded by CPU, memory, disk or network, and engineering consists of deciding which will bind first and provisioning past the workload. A conventional store serving many distributed clients is frequently network-bound, because each request carries a full result whether or not anything moved.
Four mechanisms attack that term. Persistent sockets remove connection setup (§11a). SAME removes the payload when nothing changed, and DIFF reduces it when something did (§11). Duplicate-work collapse gives N callers asking one question a single origin execution (§11.12) — which attacks computation, not only bandwidth. And genuinely independent work runs concurrently in the daemon’s worker pool (§11a.1a), so added cores become throughput rather than idle hardware. Collapse duplicate work; run independent work in parallel.
What remains after that is more likely to be bounded by CPU than by the wire, which is the better problem to have: cores can be added and this architecture can use them. An operator SHOULD provision the elected host against the term expected to bind, and SHOULD verify which one does rather than assuming (§14.1a).
Status. Structural. Not a defect.
4. Node ceiling: 58 on a single-pond broker, as few as 16 on a shared one (fix planned)
Mechanism. The per-tunnel address allocator bounds the number of addresses available within one pond.
Consequence. A ceiling of roughly fifty-eight nodes in a single-pond broker, and as few as sixteen where one broker carries a second pond. Larger deployments span multiple ponds joined by the broker (§7) rather than growing one pond — and an implementation MUST NOT assume the single-pond figure holds on a shared broker.
The arithmetic. Every tunnel the broker builds needs a carrier block: a point-to-point link at each end, joining two nodes through the broker's namespace. Blocks are allocated from a reserved /16, but the current algorithm uses only a band of it, in fixed groups:
reserved transit range /16 = 65,536 addresses
current algorithm 54 blocks x 31 tunnels = 1,674 tunnels
full mesh needs n(n-1)/2 tunnels
n(n-1)/2 <= 1,674 -> n = 58
Two effects make the practical figure worse. Bands are spaced so each pond's begins four steps after the last, so a broker running a second pond leaves the first with four blocks instead of fifty-four — 124 tunnels, sixteen nodes. Nothing about the first pond changed; the room it was free to grow into now belongs to another. And each tunnel consumes eight addresses where it uses four: carrier blocks are allocated as pairs of four-address subnets, two addresses of each being subnet bookkeeping that a WireGuard link has no use for, having neither broadcast nor address resolution. Half the space goes to a convention that does not apply.
A larger range is not the fix, and an implementation MUST NOT treat it as one. Tunnels in a full mesh grow as the square of the nodes, so addresses buy nodes back at a square root:
stop wasting half of each block 2x tunnels -> 1.4x nodes
use the whole reserved range 5x tunnels -> 2.2x nodes
hand transit ALL of 10/8 [RFC1918] 256x tunnels -> 16x nodes
The last line settles it: 256 times the space — every address the mesh itself lives in, which is impossible — buys sixteen times the nodes. The size of the address space is not the defect. Allocating anything at all per tunnel is the defect. While the scarce resource is spent per tunnel and tunnels are quadratic, the ceiling stays low however much of the resource exists.
The allocator is broker-side, in frognet_broker_v4.py, which ships inside broker/full_broker.tgz rather than as loose files. Status. The replacement in development moves allocation from per tunnel to per node: one carrier address per node rather than one per peer it talks to. WireGuard has carried many peers on one interface since it existed, routing among them by key rather than by interface.
On exhaustion the allocator raises; it does not wrap, reuse, or degrade. Two independent ceilings, both hard:
The arithmetic above is readable directly from this: bands begin at 10.253.200 and run to .253, giving fifty-four; each band carries thirty-one tunnels; and base = local_index * 8 is the eight addresses consumed where four are used. A pond index advances four per pond, which is the four-blocks-instead-of-fifty-four effect stated above.
A conforming implementation MUST fail an allocation it cannot satisfy rather than issuing an address already in use. Silently reusing a carrier block would produce two tunnels believing they own one link, which presents as intermittent loss on both rather than as an allocation error.
5. Memory model is one rule, not six
Mechanism. Last write wins against the key, with a freshness window the store evaluates against the row's own update time (§9.4), plus process-owned tuples reaped by the periodic sweep after their writer stops. That is the whole of it.
Consequence. Handable authority, versioning, leases, transactions, optimistic updates and single-writer regions are not provided. An application needing one today has to build it — which is exactly the class of code this model exists to stop people writing.
Status. Open; named as direction, not as commitment.
6. SAME/DIFF/FULL thresholds are judgement
Mechanism. The thresholds at which the codec selects between full, differential and identity replies are set by judgement rather than derived from measurement.
Consequence. There exist payload shapes for which a different threshold would send fewer bytes. No correctness property depends on the choice; only efficiency does.
7. Body-mode detection is heuristic
Mechanism. The codec classifies a body by inspecting it, never by its declared content type (§10.7). Five content modes are registered — json, xml, html, text, raw — plus one media mode registered separately for the SotF codex.
Consequence. Misclassification to raw forfeits compression for that class: a raw body learns no template and every send of it crosses FULL (§11.7). It does not produce a wrong answer — a raw body is transmitted whole and correctly — so the failure is a cost, not a fault. An empty or undecodable body classifies raw by construction.
8. Receive-side audio priority
Mechanism. Outbound, audio takes precedence over video on the shared wire: a voice packet never waits behind a keyframe. Inbound, frames are handled in arrival order.
Consequence. The rule that holds in one direction does not hold in the other. It is one rule applied in one more place.
Status. Open. Bounded, with an obvious oracle; a good first contribution.
9. The relay cannot name whose frame it discarded
Mechanism. The media relay reports backpressure in aggregate.
Consequence. Reduced since the aggregate report stopped moving the rate: the rate now derives from what participants publish about themselves, and a viewer reports its own condition directly. What survives is that the relay still cannot attribute a discard.
Status. Partially closed, 11 August 2026. Per-sender attribution is information the relay already holds and does not use.
10. One stream, one rate
Mechanism. The media relay forwards one stream and bounds the call rate to the slowest participant.
Consequence. A fast participant is held to a slow one for as long as both are in the call. This stands until the relay can send a client something other than what arrived.
11. Simulator exclusions
What the simulator does not model, stated as of the closed feedback loop rather than as of first release. A conforming test suite MUST NOT claim anything in the first column.
Not modelled
Because
Concurrency
Merges are serialized. A race that requires two merges in flight cannot be produced.
Packet loss and jitter on a topology
The wire delivers between topology nodes. Per-edge latency is modelled and is calibrated from measurement (§17.11.1). Loss, jitter, bandwidth and outage are modelled in the transport tier and the real media ladder has been driven over them — but a topology edge cannot yet carry those parameters, so a degraded bearer cannot be attached to a named leg of a multi-site pond.
11.1 Correspondence with hardware, and what it covers
An earlier statement of this section said that no simulated result had been confirmed by a real deployment. That is no longer accurate and MUST NOT be repeated. A closed feedback loop exists in both directions. The three mechanisms are specified in §16.5 and are not restated here; what belongs in this section is their status.
Direction (§16.5)
Status
Hardware blesses the model
20 of 20 topologies recorded source: hardware; the offline engine reproduces every one
Hardware calibrates the model
In use; guessed edge timings replaced by measurement
Hardware faults become tests
In use
Provenance MUST be recorded per baseline, because it decides severity: a mismatch against a hardware baseline is a regression — the offline code no longer reproduces blessed kernel truth — while a mismatch against an offline baseline is drift since the last blessing and may be intended.
Correspondence covers committed route tables and reachability, gated against hardware truth.Discovery convergence cycle counts are recorded but not gated, because timing varies; an implementation MUST NOT treat a simulated cycle count as a predicted one. As everywhere in this specification, convergence here means discovery reaching a stable derived topology and nothing else (§2c, §6.7). Nothing here establishes that a simulated throughput or bandwidth figure predicts a deployed one; those remain separate measurements of separate workloads (§14).