01 — REALTIME MESSAGING
Managed WebSocket pub/sub, presence and direct messaging, with SDKs for JavaScript, Python and Flutter.
It’s the messaging layer. You build the interface and Metered runs the connections.
Free on every account, no credit card required
A Canadian company building real-time software since 2013. 25,000+ apps have used Metered TURN.
import { SignallingClient } from "@metered-ca/realtime";
const client = new SignallingClient({
apiKey: "pk_live_…",
});
await client.connect();
await client.subscribe("alerts/critical");
// Messages from every other subscriber
client.on("message", ({ from, data }) => {
console.log(from, data);
});
await client.publish("alerts/critical", {
sensor: "T-12",
value: 92,
});
THE TOUR
02 — HOW IT WORKS
Clients hold one WebSocket to wss://rms.metered.ca/v1 and exchange single JSON frames. Your backend drives the same service over a REST control plane, without keeping a socket open.
CHANNELS
A publish reaches every other subscriber of the channel. Keys are scoped to channel patterns such as app_42/*.
DIRECT
Send to a single peer ID with no channel in between: the path WebRTC offers, answers and ICE candidates take.
PRESENCE
A full roster when you subscribe, then join and leave events, each peer carrying the identity your backend signed.
REST
Mint tokens, publish from your server, list a channel’s peers, disconnect a peer and read usage.
// connect
wss://rms.metered.ca/v1?token=<jwt>
// ← welcome, the first frame after auth
{ "type": "welcome", "peerId": "peer-3",
"maxMessageSize": 65536, … }
// → join a channel
{ "type": "subscribe", "channel": "room-42",
"requestId": "s1" }
// ← acknowledged, then the full roster
{ "type": "ack", "requestId": "s1" }
{ "type": "presence", "channel": "room-42",
"joined": [{ "peerId": "peer-7" }], "left": [] }
// → broadcast to everyone else on the channel
{ "type": "publish", "channel": "room-42",
"data": { "text": "hi" } }
// → or message one peer directly
{ "type": "send", "to": "peer-7",
"data": { "kind": "offer", "sdp": "…" } }
03 — PRESENCE
The first presence event after you subscribe is the full roster, even when the channel is empty. After that you get joins and leaves. Every peer carries the identity your backend put in its token, such as a user ID, display name and avatar, so an online indicator needs no extra lookup.
// Your server signed each peer's identity into
// its token: peerMetadata { userId, username }
const online = new Map();
client.on("presence", ({ joined, left }) => {
// First event: the full roster. Then changes.
for (const p of joined) {
online.set(p.peerId, p.metadata);
}
for (const p of left) online.delete(p.peerId);
renderOnlineList(
[...online.values()].map((m) => m?.username),
);
});
await client.subscribe("team-lobby");
04 — USE CASES
The same protocol carries every one of these.
Exchange offers, answers and ICE candidates, with Metered TURN credentials delivered on connect.
How it works with TURNA planner publishes subtasks to a workflow channel and agents reply directly. Long tool calls stream back as a sequence of direct messages. A Python voice agent can also join a call and talk with your users.
One channel per room, author identity on messages when you opt in, and moderation from your server: kick a peer or post a system message.
Presence and chat guideEach device gets a narrowly scoped token, publishes telemetry on its own channel and listens for commands your backend sends over REST.
High-frequency cursor and state updates on a per-document channel, with identity cached from presence so each message stays small. For peer-to-peer traffic, open a WebRTC data channel.
Peer-to-peer data channel guidePublish to a user’s or team’s channel straight from your backend with one REST call. No WebSocket on the server side.
REST control plane05 — WEBRTC SIGNALLING
When your app has an active Metered TURN service, on any tier including free, each connection’s welcome message carries ready-to-use TURN credentials by default. Pass them straight to RTCPeerConnection: no separate credential fetch, no backend call.
import { MeteredPeer } from "@metered-ca/realtime";
// Metered TURN credentials arrive with the connection
const peer = new MeteredPeer({
tokenProvider: () => fetchJwt(),
});
peer.on("peer-joined", ({ peer: remote }) => {
remote.on("stream-added", ({ stream, metadata }) => {
if (metadata?.role === "screen") showScreen(stream);
else showCamera(stream);
});
});
peer.on("peer-left", ({ peer: remote }) => {
removeTiles(remote.id);
});
// Reconnects are automatic; show a banner meanwhile
peer.on("state-change", ({ to }) => {
setBanner(to === "reconnecting");
});
const cam = await navigator.mediaDevices
.getUserMedia({ video: true, audio: true });
peer.addStream(cam, { role: "camera" });
await peer.join("room-42");
06 — METERED TURN
Realtime Messaging establishes the session. Metered TURN carries the media, relaying WebRTC traffic through NATs and firewalls. Use them together, or each on its own.
Explore Metered TURN07 — SDKS
All three speak the same wire protocol, so a browser, a Python agent and a Flutter app can share one channel. Each has the same two classes, MeteredPeer for calls and SignallingClient for pub/sub, reconnects for you, and is MIT-licensed.
JAVASCRIPT · TYPESCRIPT
Browser, Node.js and React Native. A small bundle with WebRTC included and no runtime dependencies, in ESM, CommonJS and UMD builds with TypeScript types.
npm install @metered-ca/realtime
PYTHON
Async Python 3.10+, with WebRTC on aiortc. Built for AI voice agents, IoT bridges, recording bots and server-side pub/sub.
pip install "metered-realtime[webrtc]"
FLUTTER · DART
Android, iOS, web and desktop, with WebRTC from flutter_webrtc and events as Dart streams.
flutter pub add metered_realtime
RAW WEBSOCKET
Any other language, from C and C++ on devices to Go, Rust, Swift and Kotlin: connect to the same WebSocket protocol directly. It’s single JSON messages, and those clients share channels with SDK peers.
08 — SECURITY AND CONTROL
// Server: mint a token for one user
const url = "https://rms.metered.ca/v1/tokens";
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${REALTIME_SK}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
peerId: userId,
channels: [`call-${callId}`],
permissions: [
"publish", "subscribe", "presence", "send",
],
expiresInSec: 3600,
peerMetadata: { userId, username },
}),
});
const { token } = await res.json();
// Browser: connect as that user
const client = new SignallingClient({
tokenProvider: fetchToken,
});
09 — PRICING
Every account can use Realtime Messaging on the free tier, with no TURN plan needed. Paid Metered TURN plans include a larger allowance in their price, with no separate subscription.
REALTIME MESSAGING · INCLUDED WITH EVERY TURN PLAN
Concurrent connections and messages per month, included in the plan price.
| PLAN | CONCURRENT CONNECTIONS | MESSAGES / MONTH | OVER THE ALLOWANCE |
|---|---|---|---|
| FreeEvery account | 100 | 100k | Hard capNo overage charges |
| GrowthTURN 150 GB · $99/mo | 1,000 | 5M | $2.50 / million messages$0.01 / extra peak connection |
| BusinessTURN 500 GB · $199/mo | 5,000 | 25M | $2.50 / million messages$0.01 / extra peak connection |
| EnterpriseTURN 2 TB and up | 20,000 | 100M | $2.50 / million messages$0.01 / extra peak connection |
Paid-plan overage is drawn from your account balance and is on by default. Connection overage is charged once per billing period for each connection your peak concurrency reaches above the allowance. The allowance becomes a hard cap if you switch overage off in the dashboard, or if your balance runs out with auto-recharge off. The free tier is hard-capped. Limits and quotas
Need higher limits, or Realtime Messaging without a TURN plan?
10 — FAQ
No. It is the messaging layer underneath: channels, direct messages and presence over managed WebSockets. You build the interface. A chat room is one channel, and the roster comes from presence events.
Realtime Messaging is built for clients — browsers, mobile apps, devices and agents — connecting directly over WebSockets with per-client scoped tokens and presence. It does not store messages: there is no history or replay, so catch-up state stays in your backend.
No. Every account gets the free tier: 100 concurrent connections and 100,000 messages a month. Paid TURN plans include a larger allowance. For WebRTC, automatic TURN credential delivery needs an active TURN service on the app, on any tier including free.
Yes. Put your own iceServers in the token's metadata and they are used instead of the automatically injected Metered TURN credentials.
A secret key pair stays on your server and signs short-lived JWTs (up to 24 hours) that scope each client to specific channels and actions. For static sites and prototypes, a browser-safe publishable key carries a fixed scope set in the dashboard.
Every SDK reconnects automatically with exponential backoff and jitter, and treats close codes such as an invalid token or an administrative disconnect as final. WebRTC sessions also recover their peer connections and reconcile channels, so remote peers survive a signalling reconnect.
JavaScript and TypeScript (browser, Node.js and React Native), Python and Flutter. Any other language, from C and C++ on devices to Go, Rust, Swift and Kotlin, connects to the same WebSocket protocol directly, and those clients share channels with SDK peers.
Yes. The JavaScript SDK runs on React Native with react-native-webrtc supplying WebRTC. Expo needs a development build rather than Expo Go.
No. It speaks its own JSON protocol over WebSockets. Devices connect with the same protocol as browsers, and your backend can publish commands over REST without holding a WebSocket open.
No. Realtime Messaging is a managed service and the server is not open source. The SDKs are MIT-licensed: @metered-ca/realtime on npm, metered-realtime on PyPI and metered_realtime on pub.dev.
100 concurrent connections and 100k messages a month, free on every account.