§ 7.1 Module 7 — From Two to Two Thousand: Data, Topologies, and Trust
March 2020: every mesh-topology product melted at four participants, and the architecture that won can read your media.Course notes
By the end of this lesson
The failure that forced the design
SCTP (2000) was built to carry SS7 telephone signaling over IP: message-oriented, multi-streamed, selectively reliable — everything TCP isn't. The internet's middleboxes, which only understood TCP and UDP, silently killed it. WebRTC performed the resurrection: run SCTP in userspace, inside DTLS, on top of UDP, and every NAT is fooled. Fifteen years late, the best transport design of its era finally shipped — as a JavaScript API.
SCTP's history as telco signaling transport (SIGTRAN, 2000), its deployment death-by-middlebox, and its resurrection in userspace over DTLS. The ordered/reliable knob space, message semantics, backpressure engineering, and a real flow-controlled file transfer.
Cross-references: SCTP & RTCDataChannel.
The operational claim is precise: SIGTRAN origins; RFC 4960; the encapsulation stack (RFC 8261/8831/8832, DCEP). Treat it as an observable contract. Record the state transition or counter that proves it, test the failure path as well as the happy path, and keep units and clock domains explicit. That discipline is what separates a plausible WebRTC explanation from a production diagnosis.
Engineering consequence. SIGTRAN origins; RFC 4960; the encapsulation stack (RFC 8261/8831/8832, DCEP). Verify the behavior with a controlled trace before interpreting a live call: establish the expected state, change one variable, and compare deltas rather than isolated values. In production, attach the observation to a timestamp and media direction so the same evidence cannot be mistaken for a different failure.
RTCDataChannel.send() queues without bound and does not block. Push a 500 MB file through it in a loop and the browser buffers all of it in memory until Chrome's roughly 16 MB internal limit throws OperationError — by which point you have already ballooned the tab, and on mobile the OS may have killed it first. The channel's actual drain rate is governed by SCTP congestion control and by whatever bandwidth the media streams left over, which is often far less than you assumed.
Implement the pattern properly: set channel.bufferedAmountLowThreshold (256 KB is a reasonable value), pause when channel.bufferedAmount exceeds a high-water mark of a few hundred KB, and resume from onbufferedamountlow. Never write a send loop without a buffered-amount check in it.
The operational claim is precise: Streams; ordered/unordered × reliability (maxRetransmits vs maxPacketLifeTime); negotiated channels. Treat it as an observable contract. Record the state transition or counter that proves it, test the failure path as well as the happy path, and keep units and clock domains explicit. That discipline is what separates a plausible WebRTC explanation from a production diagnosis.
Engineering consequence. Streams; ordered/unordered × reliability (maxRetransmits vs maxPacketLifeTime); negotiated channels. Verify the behavior with a controlled trace before interpreting a live call: establish the expected state, change one variable, and compare deltas rather than isolated values. In production, attach the observation to a timestamp and media direction so the same evidence cannot be mistaken for a different failure.
Interactive minigame
The send buffer — bufferedAmount is the only signal you get
A 3D instrument you drive yourself, one variable at a time. It needs JavaScript and WebGL, so it is not shown in this static copy of the page.
The operational claim is precise: Message orientation, EOR/max-message-size interop, m=application SDP section. Treat it as an observable contract. Record the state transition or counter that proves it, test the failure path as well as the happy path, and keep units and clock domains explicit. That discipline is what separates a plausible WebRTC explanation from a production diagnosis.
Engineering consequence. Message orientation, EOR/max-message-size interop, m=application SDP section. Verify the behavior with a controlled trace before interpreting a live call: establish the expected state, change one variable, and compare deltas rather than isolated values. In production, attach the observation to a timestamp and media direction so the same evidence cannot be mistaken for a different failure.
The negotiated limit comes from a=max-message-size (RFC 8841) and is exposed as pc.sctp.maxMessageSize. Chrome advertises 262144 (256 KB); Firefox advertises effectively unbounded (1073741823) because it supports partial delivery. So a 1 MB message that works Firefox-to-Firefox throws when the peer is Chrome, and the interoperable safe size for arbitrary peers — including non-EOR implementations — is 16 KiB.
Chunk at 16 KiB with your own framing (sequence number plus total count) and reassemble. Read pc.sctp.maxMessageSize and use it as an upper bound rather than a target. Do not skip the framing on the assumption that both ends are the same browser.
The operational claim is precise: bufferedAmount backpressure; head-of-line-blocking control per stream. Treat it as an observable contract. Record the state transition or counter that proves it, test the failure path as well as the happy path, and keep units and clock domains explicit. That discipline is what separates a plausible WebRTC explanation from a production diagnosis.
Engineering consequence. bufferedAmount backpressure; head-of-line-blocking control per stream. Verify the behavior with a controlled trace before interpreting a live call: establish the expected state, change one variable, and compare deltas rather than isolated values. In production, attach the observation to a timestamp and media direction so the same evidence cannot be mistaken for a different failure.
Start with the claim “SIGTRAN origins; RFC 4960; the encapsulation stack (RFC 8261/8831/8832, DCEP)” and turn it into a falsifiable trace. Write down the input condition, the expected state transition, the counter or packet field that should change, and the deadline by which it must change. This prevents a familiar mistake: observing a symptom after several mechanisms have already reacted and assigning it to whichever mechanism has the most recognizable name. The causal order matters. A queue grows before loss; an ICE path fails before a restart selects a new pair; a decoder loses reference state before frames stop rendering. Preserve that order in the evidence.
Now hold every unrelated variable still. Use canned timestamps, SDP, stats-shaped records, or a permission-free loopback source so the same input can be replayed. Record both media directions separately. For cumulative counters, retain two samples and divide the difference by their timestamp interval. For state machines, retain the ordered event sequence rather than the last state alone. For negotiated features, compare the offer, answer, and live stats result. These three views answer different questions: what was proposed, what was accepted, and what the browser actually used.
The first lab, The Flow-Controlled File Transfer, applies this discipline to a bounded implementation problem. On a real loopback, create reliable-ordered, unordered-unreliable, and file-transfer channels; implement chunked transfer of a 5 MB ArrayBuffer with bufferedAmountLowThreshold backpressure and seq-tagged reassembly. Assertions: received buffer hash-identical; sender never exceeds the bufferedAmount bound (harness monitors — catches buffer flooding); backpressure event actually engaged ≥1 time; .ordered===false verified on the unordered channel. Before coding, identify the invariant hidden in that brief and the tempting shortcut the tests should reject. Then run the reference trace, perturb one boundary value, and explain why the result changes. If the code passes but the explanation cannot predict the perturbed result, the implementation is memorized rather than understood.
{ ordered: false, maxRetransmits: 0 } approximates a datagram channel, but you still pay SCTP framing (a 12-byte common header plus a 16-byte DATA chunk header) and, crucially, SCTP congestion control still applies. A data channel will not exceed roughly TCP-like throughput on the path, and it shares the transport's bandwidth estimate with audio and video — a bulk transfer will visibly degrade the call it is sharing a BUNDLE with. Setting both maxRetransmits and maxPacketLifeTime throws; you get one or the other.
Rate-limit bulk data channel transfers explicitly rather than expecting congestion control to be fair to your own media. If you need real datagram semantics with independent flow control, that is what WebTransport is for.
A browser loopback is deliberately cleaner than the internet. It removes signaling latency, hostile NAT behavior, relay geography, wireless contention, device heat, and competing encoders. That makes it ideal for proving protocol and API invariants, but a dangerous basis for capacity claims. Carry the invariant into a second test with a canned hostile trace: reordered packets, a delayed candidate, a missing remote stats object, a bitrate step, or a state transition that recovers before the debounce timer. The implementation should remain correct even when optional fields are absent and events arrive more than once.
Feature detection is also part of correctness. Browser support for a codec, scalability mode, encoded transform, or transport API does not imply that the negotiated peer supports it or that hardware acceleration is active. Prefer capability checks, preserve a standards-based fallback, and label any simulation honestly. The graded result should measure the learner's algorithm; an optional live coda may demonstrate the real API without making a camera, operating-system codec, or network service a hidden test dependency.
Finally, connect the mechanism to the last topic in the lesson: “bufferedAmount backpressure; head-of-line-blocking control per stream”. Advanced WebRTC failures cross boundaries. Congestion control changes encoder output; layer selection changes keyframe demand; end-to-end encryption constrains what an SFU can inspect; a watchdog can collide with negotiation; observability can mislabel a path if its joins are wrong. At every boundary, define ownership and an idempotent action. A retry must not create duplicate work, a stale event must not overwrite newer state, and an escalation must have a terminal cleanup path.
Use the problem set as an executable notebook. Keep a short record beside each answer: the invariant, the decisive evidence, the rejected alternative, and the production caveat. That record is more valuable than a green checkmark because it can be reused when a real call presents the same mechanism through noisier evidence.
A common error
Symmetric NAT always kills P2P — direct traversal fails roughly when both sides have address/port-dependent mapping; one-sided symmetric usually connects.
Implement the decision rules, run every assertion, then use the explanations to distinguish a wrong result from a wrong model.
The Flow-Controlled File Transfer. On a real loopback, create reliable-ordered, unordered-unreliable, and file-transfer channels; implement chunked transfer of a 5 MB ArrayBuffer with bufferedAmountLowThreshold backpressure and seq-tagged reassembly. Assertions: received buffer hash-identical; sender never exceeds the bufferedAmount bound (harness monitors — catches buffer flooding); backpressure event actually engaged ≥1 time; .ordered===false verified on the unordered channel.
Lab 1
The Flow-Controlled File Transfer
Graded in the browser against 3 assertions; the editor and harness require JavaScript.
Check: The Reliability Menu. Match use cases (game state, chat, file sync, live telemetry) to channel configs; items correct 'data channels are P2P WebSockets' and the ordering-vs-reliability independence misconception.
Check your understanding
Auto-graded check
4 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m7-l1-ex2)
What the specifications say, and what the browsers actually do, are two different documents. These are the divergences that cost production teams their weekends.
With in-band negotiation, the answering side learns about a channel only after the SCTP association is established, so ondatachannel can fire hundreds of milliseconds after connectionState goes connected. createDataChannel(label, { negotiated: true, id: 0 }) on both sides eliminates that wait, but you must assign ids yourself and respect RFC 8832's parity rule: the DTLS client uses even stream ids, the server odd. Get it wrong and the channel silently never opens.
Use negotiated channels with explicit ids for control channels where setup latency matters, and derive parity from the DTLS role rather than hard-coding. Also create at least one data channel before the first offer if you know you will need one, so m=application is present from the start and you avoid a renegotiation.
WebRTC: From First Principles · Module 7, Lesson 1