Skip to main content

Why rtc.io

A short, friendly explainer of what we built rtc.io for, the use cases that pulled us toward writing it, and how it relates to the libraries we love and learned from — especially socket.io, which rtc.io extends rather than replaces.

Standing on socket.io's shoulders

rtc.io is, fundamentally, a thin layer on top of socket.io. The signaling client is a subclass of socket.io-client's Socket; the server (rtc.io-server) extends socket.io's Server. emit, on, once, namespaces, the wire protocol, the reconnection handling, the room model on the server — every bit of that is socket.io's, unchanged.

We picked socket.io because it is the API the JS ecosystem already knows, because the maintainers have spent a decade making it robust under real-world networks, and because building peer-to-peer on top of an event bus that everyone already trusts is a much better starting point than reinventing one. If rtc.io feels familiar, that's why — and the credit is socket.io's.

What rtc.io is for

We wrote rtc.io because we kept finding ourselves in the same shape of problem: a small group of browser users (often two, sometimes up to a handful) need to send media and data to each other directly, with a server in the middle only long enough to introduce them. The use cases that pulled us toward this shape:

  • 1-on-1 calls — sales, support, telehealth, tutoring, customer success. Two parties; latency and privacy matter; an SFU is overkill.
  • Small group meetings (≤ 6–8 people) where mesh bandwidth is fine and a centralized media server adds cost without adding value.
  • Collaborative cursors and presence in browser apps — Figma-style multiplayer dots, live typing indicators, shared selection — where a server round-trip is felt.
  • Browser-to-browser file drop with proper backpressure — moving big files between two known parties without uploading them anywhere.
  • Game state and pose tracking over unordered, unreliable channels (one packet supersedes the last; a retransmit is just stale data).
  • IoT and robotics dashboards where a relay in the middle adds avoidable latency.
  • Anywhere you'd reach for socket.io for chat, and then realise you'd rather the chat not pass through your server at all.

For all of those, the friction point was always the same: the 80% of the work is the connection — perfect negotiation, ICE restart, transceiver reuse, glare resolution, late-joiner replay, DataChannel backpressure — and the application logic is the 20% you actually wanted to write. rtc.io exists to make that 80% disappear behind emit and on.

The libraries we learned from

The WebRTC and real-time landscape on the web is genuinely good. Each of the libraries below taught us something, and rtc.io exists alongside them, not in opposition to them.

socket.io

The transport, the API, the philosophy. rtc.io is built on it and inherits everything from it. Anywhere rtc.io feels well-shaped, that shape came from socket.io. We're enormously grateful to the maintainers and the community.

peerjs

peerjs has been around since 2013 and has introduced more developers to WebRTC than probably any other library. Its friendly peer.call(id, stream) / peer.connect(id).send(data) API is the one most people remember when they think "WebRTC, but easy." If you have a peerjs project running today and it does what you need, that's a great outcome — keep it.

We started rtc.io because our app naturally wanted multiple named channels per peer (a chat broadcast plus a file channel plus an unordered cursor channel), and we wanted the channel set to be a first-class concept that late joiners inherit automatically. peerjs is built around one DataChannel + one media slot per connection by design — that's the right choice for many apps and the wrong one for the shape we kept building, so we wrote rtc.io for ours rather than fight peerjs on its home turf.

simple-peer

simple-peer is honest about what it is — a clean, well-tested wrapper around RTCPeerConnection that gives you offers, answers, candidates, and lets you transport them yourself. It's an excellent choice when you already have a signaling protocol you like and just want the WebRTC primitive with sensible defaults.

rtc.io takes the opposite end of that contract: we wanted the signaling protocol to also be solved, by socket.io, so that "open a peer connection between Alice and Bob" was one line on each side. If you're hand-rolling signaling already, simple-peer might be the better fit — and you can mix and match: rtc.io is happy to coexist.

mediasoup, LiveKit, Janus, Jitsi

These are SFUs — selective forwarding units. They terminate every peer's stream on a server and forward it to subscribers. They are the right answer for:

  • Large rooms (10+ participants) where mesh bandwidth blows up.
  • Server-side recording and composition.
  • Webinar-style broadcast to thousands of viewers.
  • Hard SLAs across heterogeneous networks (simulcast, SVC).
  • PSTN dial-in, transcription, server-side moderation.

They are tremendous projects and they solve hard problems that rtc.io deliberately does not try to solve. If your room sizes are big or your media has to live on the server side, an SFU is the right tool — and rtc.io is happy to live next to one in the same app (use the SFU for the all-hands, use rtc.io for the 1-on-1 sales call that came out of it).

The fundamental things we wanted differently

To be concrete, the four things below are what pushed us toward writing a new library rather than wrapping an existing one. They aren't faults of any other library — most of them are intentional design decisions in those libraries that simply don't match the shape of what we kept building.

1. Multiple named channels per peer, as a first-class idea

Our apps wanted chat (ordered, broadcast), cursors (unordered, lossy, broadcast), and file (ordered, per-peer, big) — at the same time, to the same peers. Each with its own delivery semantics, backpressure budget, and lifecycle.

rtc.io models that directly:

socket.createChannel('chat', { ordered: true });
socket.createChannel('cursors', { ordered: false, maxRetransmits: 0 });
socket.peer(id).createChannel('file', { ordered: true });

All three coexist on the same peer connection, share the SCTP transport, and are matched between sides by name (no DC-OPEN handshake; we use negotiated:true with a deterministic SCTP stream id).

2. Streams are routable through emit

Most libraries split media (peer.addStream / peer.call) from messaging. We wanted streams to flow through the same event bus our application already uses:

socket.emit('camera', new RTCIOStream(local));
socket.on('camera', (remote) => { videoEl.srcObject = remote.mediaStream; });

The library detects the RTCIOStream payload, attaches transceivers, registers the stream for replay, and on the receiving side fires the same 'camera' event handler your other code already uses. One mental model, one API.

3. Late joiners are a default, not an exercise

Every stream you emit is registered. When peer N joins after a screen share has already started, they receive that share automatically — no application code has to remember to re-broadcast it. The same is true for broadcast channels: a late joiner enters the channel without anyone having to call createChannel on their behalf.

4. Backpressure as a built-in contract

RTCDataChannel exposes bufferedAmount and an event but no queue, no watermarks, no budget. Every app shipping a file transfer reinvents the same loop. rtc.io ships it: a high-water mark (16 MB), a low-water mark (1 MB), a per-channel queue budget, send() returns false when you should back off, and 'drain' tells you when to resume.

What rtc.io is not

  • Not an SFU. If you need 30+ person rooms, recording, or server-side moderation of media, run mediasoup or LiveKit. We're happy to coexist.
  • Not a replacement for socket.io. Weare socket.io, with peer-to-peer added on top.
  • Not a hosted SaaS. We run a free public signaling server at server.rtcio.dev for demos and prototypes (with the caveats below), but the expectation is you self-host once you ship — it's an npm i rtc.io-server away.
  • Not a media engine. Codec selection, audio processing, jitter buffers — that's the browser's job.

About the public signaling server

⚠️ Important: rooms on `server.rtcio.dev` are global and unauthenticated.

The free public server we host is shared with everyone using rtc.io for prototypes and demos. Anyone who joins a room with the same name will land in the same call, including strangers. The server has no concept of room ownership, no authentication, and no way to tell two unrelated apps apart.

For prototyping, generate a hard-to-guess room id (a UUID, or 16+ random characters — see crypto.randomUUID()). For anything real, please run your own server — it's a single npm install and a 30-line file. That gives you authentication, room ownership, persistence, and full control over who can join what.

A small comparison, when it helps

The matrix below is offered for "I just need to pick one" cases. Every cell is a simplification — the prose above has the nuance, and every library named here is a great choice for the use cases it was built for.

Capabilityrtc.iopeerjssimple-peerSFU
TopologyMesh (P2P)Mesh (P2P)Mesh (P2P)Star (server)
Sweet-spot room size2–82–42–410–10,000+
Multiple named channels per peerbuilt-innot the focusbuild it yourselfvaries
Late-joiner stream replayautomaticapplication-levelapplication-levelhandled by server
DataChannel backpressure helpersincludedapplication-levelapplication-leveln/a
Familiar API shapesocket.ioEventEmitterEventEmittervendor SDK
Self-hosted signalingrtc.io-serverpeerjs-serverroll your ownthe SFU is the server
Server cost at scale≈ socket.io≈ socket.io≈ socket.ioCPU-heavy (transcode)
Recording, server-side compositionincluded

When rtc.io is the right choice

  • 1-on-1 video calls (sales, support, telehealth, tutoring).
  • Small-group calls and rooms (≤ 8 people).
  • Collaborative cursors / presence / live editing — broadcast channel + ctrl channel.
  • Browser-to-browser file transfer with proper backpressure.
  • Game state sync (unordered maxRetransmits:0 channels).
  • Anywhere you'd reach for socket.io and want the data plane to skip the server.

When something else is the right choice

  • 30+ person all-hands or webinars → an SFU (LiveKit, mediasoup).
  • Server-side recording or composition → an SFU.
  • PSTN dial-in / SIP integration → a SIP gateway (Daily, Twilio, an SFU with SIP).
  • You already have a signaling protocol you love → simple-peer.
  • You only need 1 DataChannel + 1 media slot and want the smallest bundle → peerjs is great for that.

The path from prototype to production

  1. Day 1. npm i rtc.io, point at server.rtcio.dev (with a random room id — see the disclaimer above), copy the Getting started snippet, ship a working call.
  2. Day 7. npm i rtc.io-server on a small box. Move signaling off the public broker. Add auth in your connection handler.
  3. Day 30. Add a TURN server (coturn / Cloudflare Calls / Twilio) for users behind symmetric NAT. Guide.
  4. Day 90. If you've genuinely outgrown mesh (10+ video streams per room), bring an SFU into the picture — rtc.io and the SFU can coexist for the parts of the app each is best at.

The honest trade-offs

  • Mesh bandwidth scales O(n²). Above ~8 video streams an SFU wins on bandwidth alone.
  • You depend on socket.io's footprint. If you don't already use it, you're adding it. We picked socket.io because it's the JS ecosystem's most-trusted event transport; we wouldn't try to replace it.
  • Mobile + battery. Mesh keeps every peer's CPU encoding to N destinations. For mobile-heavy apps with 5+ peers, profile carefully.
  • You still need a TURN server in production. ~10–15% of users are behind a NAT that requires relay. That's WebRTC, not us — but it's a real cost.

The summary

rtc.io is the answer when you want WebRTC's economics (free P2P bandwidth, zero media on your server) with socket.io's ergonomics (the API everyone already knows), for room sizes most apps actually have (2–8). For everything else, the libraries we mentioned above are excellent — and we'd rather you use the right tool than the one we wrote.

If you've never written WebRTC and were about to, please read How it works before you reach for the spec. It will save you a month either way.

Get startedExamplesHow it worksSource on GitHub