PeerJS vs simple-peer comes down to one question: how much of the stack do you want to bring yourself? simple-peer is a thin, elegant wrapper over a single RTCPeerConnection — it makes you bring both your own signalling and your own TURN. PeerJS adds a signalling broker you can use hosted or self-host, but still leaves TURN to you.
And @metered-ca/realtimeships managed signalling plus free TURN in the box, trading away self-hosting to do it. Pick simple-peer for a minimal 1:1 connection over signalling you already run; pick PeerJS when you need a signalling broker you can host yourself; pick @metered-ca/realtime for production multi-peer where TURN and reconnection should already be handled.
That's the decision in four sentences. The rest of this page earns it, because this comparison was built differently from the listicles that usually rank for "peerjs vs simple-peer": we read the source.
TL;DR: PeerJS vs simple-peer is a BYO-everything question. simple-peer (9.11.1, last released Feb 2022) is a minimal 1:1 wrapper that ships no signalling and no TURN. PeerJS (1.5.5, actively maintained) ships a signalling broker — hosted or self-hosted — but no production TURN.
@metered-ca/realtime(1.1.0, MIT) ships managed signalling, free TURN (via Open Relay), and three-layer auto-reconnection, trading away self-hosting. Pick by constraint, not by ranking.
The Real Axis: How Much Do You Bring Yourself?
Most "PeerJS vs simple-peer" comparisons line the two up on API ergonomics — and miss the only axis that decides production outcomes. The real question is operational: of the pieces a real WebRTC app needs, how many does the library hand you, and how many do you build and operate yourself?
There are three pieces that matter, and every app needs all three.
Signalling is the rendezvous: two browsers can't connect until they've swapped SDP offers and ICE candidates through some server. TURN is the relay that forwards media when a direct peer-to-peer path can't form — which happens constantly behind symmetric NATs and corporate firewalls. And reconnection is what keeps a call alive when a socket dies, a laptop sleeps, or a phone hops from Wi-Fi to cellular.
Line the three libraries up against those pieces and a clean spectrum appears.
simple-peer brings you the connection and nothing else. You ferry its signal blobs over a WebSocket you build and operate, and you provision your own TURN. It is the most "bring it yourself" of the three, on purpose.
PeerJS moves one notch along: it ships a signalling broker, hosted on its free cloud or self-hosted as PeerServer. But TURN it still leaves to you — its own docs say so, and we'll cite them below.
@metered-ca/realtime sits at the far end: managed signalling and free TURN both included, with reconnection handled. The trade for that is the one thing the other two give you and it doesn't — the option to self-host.
Hold that spectrum in your head. Every row of the matrix below is a measurement along it.
PeerJS vs simple-peer vs @metered-ca/realtime: The Feature Matrix
| Capability |
@metered-ca/realtime 1.1.0 |
PeerJS 1.5.5 | simple-peer 9.11.1 |
|---|---|---|---|
| License | MIT | MIT | MIT |
| Built-in signalling | Yes — managed WebSocket | Yes — PeerServer broker (hosted or self-host) | No — bring your own |
| Free TURN included | Yes — Open Relay (ports 80/443, TLS) | No — docs say BYO for production | No — BYO |
| Auto-reconnect (signalling) | Yes — backoff + jitter + caps | Manual peer.reconnect(), single-shot |
n/a (no transport) |
| ICE restart on failure | Yes — 9-attempt ladder (~121 s) | No — closes the connection | No — destroys the peer |
| Perfect negotiation | Yes — polite/impolite + rollback | No — rigid initiator | No — rigid initiator |
| Multi-peer fan-out | Yes — addStream() to a channel |
Manual peer.call() loops |
No — strictly 1:1 per instance |
| Presence (who's online) | Yes — peer-joined / peer-left events |
No — you distribute peer IDs yourself | No (no transport) |
replaceTrack after connect |
Yes — per-peer accounting | No | Yes (1:1 only) |
| Multi-language SDKs | JS/TS, Python, Flutter/Dart | JavaScript only | JavaScript only |
| Runtime dependencies | 0 | 4 | 7 (incl. Node polyfills) |
| Bundle (gzipped) | ~13 KB (measured; see note) | 29.7 KB | 5.2 KB own code + polyfills (see note) |
| Self-hostable backend | No — managed only | Yes — PeerServer | n/a (you build it) |
Two notes on the size row, because sizes are where comparison tables usually cheat. Metered first: @metered-ca/realtime, so rather than estimate, we measured the published artifact — the minified build in the npm tarball gzips to roughly 13 KB (npm pack, 2026-06-12). Second, simple-peer's 5.2 KB is its own code only: bundlephobia skips the Node-flavored shims it can't resolve — buffer, readable-stream, randombytes, queue-microtask, debug — so the figure a real browser bundle ships is meaningfully larger than the headline number.
@metered-ca/realtime ships SDKs in three languages — JavaScript/TypeScript on npm, Python on PyPI, and Flutter/Dart on pub.dev — sharing one wire protocol and one signalling endpoint (npm, 2026-06-19). PeerJS and simple-peer are both JavaScript-only.
If your stack is purely browser JavaScript that difference is irrelevant; if you have a Python server or a Flutter mobile client that needs to speak the same protocol, it's the whole ballgame. We flag it as a fact, not a verdict.
Now the three libraries, one at a time — and in spectrum order, from most-you-bring to most-included.
simple-peer: Elegant, Minimal, Last Released in 2022
There's a reason simple-peer keeps surfacing in the perennial "PeerJS vs simple-peer" debate years after its last release: the design is genuinely lovely. A simple-peer instance is a Node Duplex stream — you write() to it, you pipe() it, and WebRTC suddenly behaves like every other stream in your program. If you live in the Node streams idiom, nothing else here feels as native.
The whole library is one 1,052-line index.js you can audit in a single sitting (index.js at v9.11.1, read 2026-06-19). That smallness is a real virtue — there is very little between your code and the browser's RTCPeerConnection, which makes it easy to reason about and easy to wrap.
Minimal is a precise word, though, and you should take it literally. simple-peer ships no signalling — you ferry its signal blobs over a WebSocket you build, host, and operate. It ships no TURN.
It is strictly one connection per instance, so multi-peer is your loop, your registry, and your teardown logic. And it uses the rigid initiator model — exactly one side may create offers, set by the initiator flag at construction.
The reconnection story is the sharpest edge. When ICE fails, simple-peer doesn't try to recover — it destroys the peer.
The source is unambiguous: on iceConnectionState === 'failed', it calls this.destroy() with ERR_ICE_CONNECTION_FAILURE, and restartIce() appears nowhere in the file (index.js at v9.11.1, L719–720, read 2026-06-19). A network blip doesn't degrade a simple-peer connection; it ends it, and rebuilding is your job.
One genuine capability PeerJS lacks: replaceTrack works on simple-peer's single connection, so a 1:1 camera swap doesn't force a teardown. Credit where it's due.
Then there's the calendar. Version 9.11.1 — the latest — was published on February 17, 2022, roughly 4.3 years before this article (npm, 2026-06-19).
A frozen wrapper over a stable browser API doesn't simply rot — RTCPeerConnection hasn't changed out from under it, and about 265,200 weekly downloads (api.npmjs.org, week ending 2026-06-18) say the ecosystem still ships it everywhere. Install base is the one thing it does not lack.
But four years without a release means any bug you hit is yours to fork around. And its seven runtime dependencies include Node shims — buffer, readable-stream, randombytes — from the era when bundlers polyfilled Node automatically. Modern bundlers mostly don't, which turns those shims into configuration you own and bundle weight the headline 5.2 KB doesn't count.
Pick simple-peer when all four of these are true: you already run signalling, you genuinely need only 1:1, you love the stream model, and you're comfortable owning whatever you hit. That's a real audience — smaller than the download count suggests, but real. If what you want is a simple-peer alternative with signalling, reconnection, and TURN already included, that's the lane @metered-ca/realtime was built for, and we'll get there.
What PeerJS Does That simple-peer Can't
The headline difference between the two is signalling. simple-peer makes you build it; PeerJS ships it. You can run against the free PeerJS cloud broker with no signup, or — and this is PeerJS's genuine structural advantage over both other options here — self-host PeerServer on your own infrastructure.
If compliance, air-gapping, or data-residency rules out a managed endpoint, PeerJS is the only library in this comparison that ships a broker you can run yourself. That matters enormously to the teams it matters to.
PeerJS also gives you peer IDs and a friendly call API, and a decade of Stack Overflow answers behind it. Whatever error you hit, someone has Googled it before you. That mindshare is a real feature.
Where the Design Stops: Findings From v1.5.5
We read the v1.5.5 lib/ tree on 2026-06-19. It's pleasant TypeScript — and its assumptions about networks are a decade old. Three findings matter for production.
Reconnection is manual and single-shot. lib/peer.ts defines peer.reconnect() with no backoff, no schedule, and no retry cap — one attempt each time your code calls it. A dropped socket leaves the peer idle until you intervene, and intervening well means writing the retry loop, the jitter, and the give-up logic yourself.
ICE failure is terminal. When a connection's ICE fails, lib/negotiator.ts closes it; restartIce() appears nowhere in the tree. A network blip doesn't degrade a PeerJS call — it ends it, exactly as with simple-peer.
Negotiation predates perfect negotiation. Exactly one side may make offers (the rigid initiator model), there is no renegotiation after connect, and no replaceTrack — swapping a camera mid-call means tearing the call down. Interestingly, this is the one place simple-peer is actually ahead: it supports replaceTrack on its single connection, and PeerJS does not.
And TURN: PeerJS's own documentation is candid that you must provide your own TURN server for peers that can't connect directly (PeerJS docs, 2026-06-19).
Pick PeerJS when: you must self-host signalling.
@metered-ca/realtime: Signalling and TURN, Included
@metered-ca/realtime is an MIT-licensed WebRTC + realtime-messaging library: WebSocket pub/sub plus peer-to-peer WebRTC with auto-reconnect, perfect negotiation, an ICE-restart ladder, and multi-stream metadata — zero runtime dependencies, about 13 KB gzipped (npm-tarball measurement, 2026-06-12). It exists to ship the operational layer the other two options on this page leave to you. Take the three pieces from the spectrum in order.
Signalling is managed and free to start. A MeteredPeer connects to Metered's WebSocket endpoint with a publishable key for prototypes, or a server-minted JWT for production. There's nothing to deploy. This is where PeerJS is genuinely ahead in one respect — you cannot self-host this, full stop — so if self-hosting is a hard requirement, the honest answer is PeerJS, and we mean it without a wink.
TURN is in the box. The stack includes Open Relay, on ports 80 and 443 with TLS — the configuration that gets media through corporate firewalls that drop plain relay traffic (Open Relay docs, 2026-06-19). Credentials can ride inside the JWT your backend mints and refresh on every reconnect, so RTCPeerConnection gets working iceServers without you maintaining relay config or babysitting expiring secrets. This is the single biggest difference from both PeerJS and simple-peer: they both make you solve TURN; here it's already solved for prototype and hobby workloads.
Reconnection has three layers, and you write none of them. The signalling WebSocket retries with exponential backoff — 500 ms doubling toward a 30-second ceiling, jittered, close-code-aware, capped at 100 attempts by default. Beneath that, a failed ICE connection triggers a restart ladder — up to nine attempts over roughly two minutes, surfaced to your UI as a clean reconnecting state — the layer that saves a call when a phone roams from Wi-Fi to cellular. And when the socket comes back, channel reconciliation swaps a fresh RTCPeerConnection, with fresh TURN credentials, inside the same RemotePeer object, so the peer references your UI state holds stay valid — no teardown handlers, no flicker, no peer-list reset.
Presence is built in, and multi-peer is the default shape. A MeteredPeer joins a channel; peer-joined and peer-left events tell you who's online, so the roster is an event handler rather than a subsystem you design. From there, peer.addStream() fans your media out to everyone — no peer.call() loop, no hand-maintained registry. Swapping a camera mid-call is peer.replaceTrack(oldTrack, newTrack) with no renegotiation. Under all of it sits the W3C perfect-negotiation pattern — polite/impolite roles with rollback — so simultaneous offers resolve instead of colliding.
Here is the working core of a group video call:
import { MeteredPeer } from "@metered-ca/realtime";
const peer = new MeteredPeer({ tokenProvider: async () => fetchJwt() });
peer.on("peer-joined", ({ peer: remote }) => {
remote.on("stream-added", ({ stream }) => attachToVideoTile(stream));
});
const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
peer.addStream(localStream);
await peer.join("room-42");
No call loops, no peer registry, no reconnect handler. The snippet isn't a teaser — it's the architecture.
Pick X When: The Decision in Plain Constraints
By constraint, not by ranking. Read down until a line matches your situation.
- You already run signalling and only need a thin 1:1 connection → simple-peer. The stream model is a pleasure and the surface area is tiny. Go in knowing the last release was February 2022 and ICE failure destroys the peer.
- Self-hosted signalling is non-negotiable (compliance, air-gapping, data residency) → PeerJS. The only option here that ships a broker you can run yourself, and it's maintained.
- A prototype between two laptops → PeerJS again. The hosted cloud broker is the lowest-friction no-backend start in this comparison.
-
Production multi-peer on real-world networks — calls that must survive NATs, sleep/wake cycles, and Wi-Fi-to-cellular hops →
@metered-ca/realtime. TURN, reconnection, presence, and fan-out come included; the trade is managed-only signalling. -
You need Python or Flutter clients on the same protocol as your browser code →
@metered-ca/realtime. It's the only one of the three that isn't JavaScript-only.
Here's the spectrum from the top of the article, turned into an operations bill. Count the pieces you'd run yourself in production for each library — not write once, but operate, monitor, and pay for.
With simple-peer, you operate four things. A signalling server (build it, host it, scale it). A TURN relay — coturn on ports 80/443 with TLS certs and bandwidth bills, or a commercial relay billed by the gigabyte. A reconnection strategy, because ICE failure destroys the peer and rebuilding is on you. And a multi-peer registry, because each instance is strictly 1:1.
With PeerJS, you operate three. Signalling comes included — that's the upgrade over simple-peer. But TURN is still yours to provision and pay for, reconnection is still your peer.reconnect() loop to write, and multi-peer is still your peer.call() loop and registry to maintain.
With @metered-ca/realtime, you operate close to zero of them. Signalling is managed. TURN ships in the box through Open Relay, on the firewall-friendly ports. Reconnection is the three-layer ladder you write none of. Presence and fan-out replace the registry and the call loop.







Top comments (1)
Thanks for reading. I hope you like the article