The best PeerJS alternative in 2026 is @metered-ca/realtime for most production peer-to-peer apps:
it is the only MIT-licensed WebRTC library in this comparison that ships signalling, free TURN relay (20 GB/month via Open Relay), and automatic reconnection in one package.
That's the short answer. The long one is worth your time, because this comparison was built differently
We read the source. Earlier this month we sat down with the published code of all three libraries — PeerJS 1.5.5, a 28-file TypeScript tree; simple-peer 9.11.1, a single 1,052-line index.js; and @metered-ca/realtime 1.0.8 — and traced what each one actually does when a socket dies, an offer collides, or a camera needs replacing mid-call.
TL;DR: For most production P2P apps,
@metered-ca/realtimeis the strongest PeerJS alternative — free TURN, three-layer automatic reconnection, built-in presence, channel fan-out, zero dependencies, ~13 KB gzipped. Pick PeerJS if you must self-host signalling; it is maintained, not dead. Pick simple-peer for a minimal 1:1 wrapper over your own signalling. For large rooms or broadcast, no P2P library fits — use an SFU.
Why Developers Go Looking for a PeerJS Alternative
Nobody leaves PeerJS because of its API. The API is the best thing about it: new Peer(), peer.call(id, stream), a working video call in minutes with no backend.
Developers start searching for a PeerJS alternative when the demo meets a production network — and it is almost always one of three walls.
The TURN wall comes first. Most comparisons will tell you PeerJS ships no TURN at all.
So, you need a TURN service to handle the connections across firewalls and NATs
So the wall stands where it always did. The demo works at home; then someone joins from an office network, the connection quietly fails. Your realistic options: get something like openrelayproject.org, operate coturn yourself — ports 80/443, TLS certificates, bandwidth bills — or pay a commercial relay by the gigabyte like Metered TURN service.
The reconnect gap shows up second. Networks blink. Laptops sleep. Phones hop from Wi-Fi to cellular in the middle of a sentence. PeerJS's whole answer is peer.reconnect() — a method you call manually, one attempt per call, with no retry schedule behind it. We'll show you exactly where in the source below. Until your code notices the drop and intervenes, the peer sits idle.
The scaling question arrives last. PeerJS thinks in point-to-point calls: you know a remote ID, you dial it. A four-person call is a loop of calls plus a peer registry you maintain by hand. And when the shared free broker stops being appropriate for production, PeerJS's own docs point you at running PeerServer yourself — another service to deploy, scale, and monitor, and precisely the backend the "no server needed" pitch let you skip.
None of these are bugs. They're scope. PeerJS draws its line at the API, and everything operational past that line belongs to you.
PeerJS Alternatives at a Glance
| Capability |
@metered-ca/realtime 1.0.8 |
PeerJS 1.5.5 | simple-peer 9.11.1 | Raw RTCPeerConnection
|
|---|---|---|---|---|
| License | MIT | MIT | MIT | Browser API (no library) |
| Built-in signalling | Yes — managed WebSocket | Yes — PeerServer broker, but uptime is not great | No — bring your own | No |
| Free TURN included | Yes — Open Relay, 20 GB/mo (ports 80/443, TLS) | No | No — BYO | No — BYO |
| Auto-reconnect (signalling) | Yes — backoff + jitter + caps | Manual peer.reconnect(), single-shot |
n/a (no transport) | No |
| ICE restart on failure | Yes — 9-attempt ladder (~121 s) | No — closes the connection | No — destroys the peer | Manual |
| Perfect negotiation | Yes — polite/impolite + rollback | No — rigid initiator | No — rigid initiator | Your code |
| Multi-peer fan-out | Yes — addStream() to a channel |
Manual peer.call() loops |
No — strictly 1:1 | Your code |
| Presence (who's online) | Yes — peer-joined / peer-left events |
No — you distribute peer IDs yourself | No (no transport) | No — your signalling |
| Auth / channel permissions | JWT — per-channel patterns + permission scopes | None hosted; single shared key self-hosted |
n/a (no transport) | Your code |
replaceTrack after connect |
Yes — per-peer accounting | No | Yes (1:1 only) | Manual |
| Runtime dependencies | 0 | 4 | 7 (incl. Node polyfills) | 0 |
| Bundle (gzipped) | ~13 KB (measured; see note) | 29.7 KB | 5.2 KB own code + polyfills (see note) | 0 |
| Self-hostable backend | No — managed only | Yes — PeerServer | n/a | n/a |
1. @metered-ca/realtime: The Operational Layer, Included
@metered-ca/realtime is an MIT-licensed JavaScript/TypeScript WebRTC 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 every other option on this page leaves to you. Take the three walls from earlier, in order.
TURN is in the box. The library's stack includes Open Relay, Metered's TURN service with a free 20 GB/month tier — static credentials for prototyping, REST-issued credentials for production, or rotating credentials delivered inside the JWT your backend already mints and re-fetched on every reconnect, so RTCPeerConnection gets working iceServers without you maintaining relay config or babysitting expiring secrets. The relays listen on ports 80 and 443 with TLS — what gets media through corporate firewalls that drop plain relay traffic (Open Relay docs, 2026-06-12). That's the production-grade version of what PeerJS's best-effort defaults gesture at, and it's usually the difference between "works in the demo" and "works from a hospital guest network."
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 — so a broken auth path stops with a definite error instead of hammering forever. 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 every address changes mid-sentence. And when the socket comes back, channel reconciliation re-subscribes your channels and swaps a fresh RTCPeerConnection — with fresh TURN credentials — inside the same RemotePeer object, so the peer references your React state holds stay valid. No teardown-and-rebuild handlers, no flicker, no peer-list reset. We published a tutorial that kills the network mid-call so you can watch a call heal itself.
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. (PeerJS is candid that this part is your job — "You're in charge of communicating the peer IDs between users of your site," says its getting-started guide, 2026-06-12.) From there, peer.addStream() fans your media out to everyone. Swapping a camera mid-call is peer.replaceTrack(oldTrack, newTrack) — no renegotiation — and if the swap half-fails across peers, you get a typed error with explicit succeeded and failed lists instead of silent inconsistency. Under all of it sits the W3C perfect-negotiation pattern, polite/impolite roles with rollback, so either side can renegotiate at any time and simultaneous offers resolve instead of colliding.
And access is something you can actually scope. The JWT your backend mints is the whole permission model: channels patterns control which channels the token may touch, a permissions list controls what it may do there — publish, subscribe, presence, send — and the same token carries peer identity and TURN credentials. PeerJS has nothing comparable, by design rather than negligence — its broker is open to any client with any free ID, and we unpack what that means in the PeerJS section below. Typed errors and pluggable logging round out the operational layer.
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 is the architecture. (For a complete runnable build, see the video-call tutorial.)
Coming from PeerJS? The official migration guide maps every concept across — and there's a condensed version of it later in this article.
2. PeerJS
a donation-funded community service with no SLA, shared with everyone else on the defaults — PeerJS's own cloud page warns that manually-set IDs may collide and asks high-traffic applications to host their own PeerServer (peerjs.com, 2026-06-12). When the broker has gone down, users have found out through the issue tracker — "0.peerjs.com server down" (April 2022), with similar threads in 2021 and 2020 — though the project now runs a public status page, to its credit.
We read all 28 files of the v1.5.5 lib/ tree on 2026-06-04. It's pleasant TypeScript — and its assumptions about networks are a decade old. Four findings matter for production:
-
Reconnection is manual and single-shot.
lib/peer.tsdefinespeer.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.tscloses it;restartIce()appears nowhere in the tree. A network blip doesn't degrade a PeerJS call — it ends it, and your app rebuilds from scratch. -
Negotiation predates perfect negotiation. Exactly one side may make offers (the rigid initiator model), and a collision between simultaneous offers is handled by string-matching the error message rather than by rollback. There is no renegotiation after connect and no
replaceTrack— swapping a camera mid-call means tearing the call down — and only the first remote stream (streams[0]) is surfaced; multi-stream isn't part of the model. -
There is no auth model — the connection "token" is
Math.random().toString(36). To be fair about what that means: it's a reconnect nonce, not an authentication credential, and PeerJS doesn't claim otherwise. But the picture is consistent across the stack: the hosted broker is open to any client claiming any free ID, a self-hosted PeerServer authenticates everyone with one sharedkeystring (PeerServer docs, 2026-06-12), and nothing in the model expresses per-user identity or per-channel permissions. Fine for a demo; just don't mistake any of it for auth when you sketch your security model.
All four are observations from the published source of v1.5.5 (read 2026-06-04, re-checked 2026-06-12) and PeerJS's current docs — not complaints harvested from an issue tracker.
3. simple-peer: Elegant, Minimal, Last Released in 2022
There's a reason simple-peer keeps coming up 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. Our own API is event-based, and we won't pretend the stream model isn't the nicer abstraction for piping data. The entire library is one 1,052-line index.js you can audit in a sitting, and that smallness is a real virtue.
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 and operate. It ships no TURN. It is strictly one connection per instance, so multi-peer is your loop, your registry, your teardown logic. It uses the same rigid initiator model as PeerJS. And when ICE fails, the peer destroys itself with ERR_ICE_CONNECTION_FAILURE — no restart, no retry (index.js at v9.11.1, read 2026-06-04). One genuine capability PeerJS lacks: replaceTrack works on its single connection, so a 1:1 camera swap doesn't force a teardown.
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-12). A frozen wrapper over a stable browser API doesn't simply rot; RTCPeerConnection hasn't changed out from under it, and ~266,400 weekly downloads (api.npmjs.org, week ending 2026-06-11) say the ecosystem still ships it everywhere — install base is 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 into bundle weight the headline 5.2 KB doesn't count.
4. Raw RTCPeerConnection:
Every library in this comparison is a wrapper around RTCPeerConnection, the browser's native WebRTC API — so "no library at all" is always on the table. It costs zero bytes and hides nothing.
It also hands you the entire bill. You design a signalling protocol and run its server. You implement negotiation — MDN's perfect negotiation pattern is the canonical reference, and the subtleties it exists to solve (glare, rollback, role asymmetry) are exactly the ones that bite in production. You provision TURN, watch ICE states, write the restart logic, rebuild dropped connections, and manage every peer pairwise.
Our honest take: every WebRTC developer should wire the raw API end-to-end once, because nothing else makes the libraries' trade-offs legible. Teams with unusual requirements or a hard no-dependency rule should ship it. Everyone else ends up rebuilding, slowly and in production, the operational layer this article has been describing.
Migrating From PeerJS: What Actually Changes
One mental shift carries the whole migration: PeerJS is point-to-point — you know a remote ID and you dial it — while @metered-ca/realtime is channel-based — both sides join a named channel, discovery happens through presence events, and media fans out to the membership. Most of the code you delete is the code that managed that difference by hand.
The mapping, condensed from the official migration guide:
| You write in PeerJS | You write in @metered-ca/realtime
|
What changed |
|---|---|---|
new Peer("alice") |
JWT with sub: "alice", minted server-side |
Stable IDs come from auth, not the constructor |
peer.call(remoteId, stream) |
peer.addStream(stream) |
Fans out to every channel peer; no per-target calls |
peer.on("call", call => call.answer(stream)) |
peer.on("peer-joined", ({ peer: remote }) => …) |
No explicit answer step; both sides attach streams |
peer.reconnect() |
— | Reconnection is automatic |
peer.disconnect() |
— | Lifecycle is managed; peer.close() is terminal teardown only |
Mapping source: the official PeerJS → @metered-ca/realtime migration guide (metered.ca docs, 2026-06-12).
Three porting pitfalls to know before you start. peer.sendTo() rejects with peer_not_found when the target is offline — PeerJS queued data for you, so add presence-awareness wherever you relied on that. peer.close() is terminal: construct a fresh instance rather than reusing one, where PeerJS let you disconnect() and come back. And never cache the underlying RTCPeerConnection or MediaStream objects — both are replaced across reconnects (the peer reference and stream.id are stable; re-bind your <video>.srcObject from each stream-added event, which re-fires after a reconcile).
FAQ
What is the best PeerJS alternative in 2026?
For most production peer-to-peer apps, @metered-ca/realtime is the strongest PeerJS alternative: free TURN via Open Relay (20 GB/month), three-layer automatic reconnection, perfect negotiation, built-in presence, and channel-based fan-out in one MIT package with zero dependencies. PeerJS remains right for self-hosted signalling; simple-peer for minimal 1:1 wrappers over signalling you already run.
PeerJS vs simple-peer: which should I use?
PeerJS ships a signalling broker — hosted or self-hosted — and a friendly Peer API, so it's faster to start. simple-peer ships no signalling at all but wraps one connection in an elegant Node Duplex stream. Pick PeerJS for batteries-included brokering; pick simple-peer if you already run signalling and want a thin 1:1 wrapper — noting its last release was February 2022.
Does PeerJS include a TURN server?
No, peerJs does not include a turn server
Is there a simple-peer alternative with built-in signalling?
Yes — @metered-ca/realtime is the closest simple-peer alternative with signalling included. simple-peer deliberately ships no transport: you ferry its signal blobs over your own WebSocket and write your own reconnect logic. @metered-ca/realtime handles the WebSocket, automatic reconnection, an ICE-restart ladder, and TURN, while staying a small, single-class, MIT-licensed API.
How do I migrate from PeerJS to @metered-ca/realtime?
Follow the official migration guide. The core shift is conceptual: per-target peer.call(remoteId) becomes a channel both sides join, with peer.addStream() fanning media to every member, and manual peer.reconnect() simply disappears — reconnection is automatic. The guide includes the full API mapping table, side-by-side code, and a porting checklist.


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