WebSocket keeps one two-way connection between a browser and your server, and every message passes through that server. WebRTC connects browsers directly to each other for audio, video and arbitrary data, encrypted with DTLS and SRTP. They are often used together: a WebSocket carries the signaling messages that set up the WebRTC call.
| Question | WebRTC | WebSocket |
|---|---|---|
| Who talks to whom | Browser to browser (peer to peer) | Browser to server |
| Built for | Audio, video, screen share, peer data | Messages between app and server |
| Setup | Offer, answer and ICE candidates via your own signaling | One HTTP Upgrade handshake |
| Delivery options | Data channels can be ordered or unordered, reliable or not | Ordered and reliable |
| Encryption | Always on (DTLS, SRTP) | Only with wss:// |
| Needs a server | For signaling, and TURN relays when peers cannot connect directly | Yes, it is the other end |
Updated
Pick what you are building in the helper below and it names the technology that fits, with the reason. It also covers the two questions people mix in with this one: webhooks, which are server to server, and plain HTTP requests.
WebRTC, with a WebSocket for signaling
Audio and video should go straight between the callers with low delay. A small WebSocket server only passes the offer, answer and candidates during setup.
The WebSocket only forwards setup messages. The STUN URL is a placeholder for the server you choose.
const signal = new WebSocket("wss://example.com/signal");
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.example.com:3478" }],
});
const channel = pc.createDataChannel("chat");
channel.onopen = () => channel.send("hi from the caller");
channel.onmessage = (e) => console.log("peer:", e.data);
pc.onicecandidate = ({ candidate }) => {
if (candidate) signal.send(JSON.stringify({ type: "candidate", candidate }));
};
signal.onmessage = async (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "answer") await pc.setRemoteDescription(msg.answer);
if (msg.type === "candidate") await pc.addIceCandidate(msg.candidate);
};
signal.onopen = async () => {
await pc.setLocalDescription(await pc.createOffer());
signal.send(JSON.stringify({ type: "offer", offer: pc.localDescription }));
};The other side gets the channel through the datachannel event rather than creating it.
const signal = new WebSocket("wss://example.com/signal");
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.example.com:3478" }],
});
pc.ondatachannel = ({ channel }) => {
channel.onmessage = (e) => console.log("peer:", e.data);
channel.onopen = () => channel.send("hi back");
};
pc.onicecandidate = ({ candidate }) => {
if (candidate) signal.send(JSON.stringify({ type: "candidate", candidate }));
};
signal.onmessage = async (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "offer") {
await pc.setRemoteDescription(msg.offer);
await pc.setLocalDescription(await pc.createAnswer());
signal.send(JSON.stringify({ type: "answer", answer: pc.localDescription }));
}
if (msg.type === "candidate") await pc.addIceCandidate(msg.candidate);
};maxRetransmits and maxPacketLifeTime cannot both be set; pick one.
const moves = pc.createDataChannel("moves", {
ordered: false,
maxRetransmits: 0,
});| Question | Webhook | WebSocket |
|---|---|---|
| Who connects | One server calls another server | A client keeps a line open to a server |
| Lifetime | One HTTP request per event | Open until either side closes it |
| Direction | Sender to receiver | Both ways |
| Typical use | Payment, repository or form events | Live chat, dashboards, presence |
| Question | HTTP request | WebSocket |
|---|---|---|
| Who starts a message | Always the client | Either side |
| Connection | Request, then response | Upgraded with 101, then stays open |
| Caching | GET and HEAD responses are cacheable | None |
| URL schemes | http://, https:// | ws:// (port 80), wss:// (port 443) |
Before two browsers can connect, each must learn the other's session description, an SDP offer and answer, and a list of possible network addresses called ICE candidates. WebRTC leaves the delivery of those messages to you.
Once they are exchanged, ICE tries the candidates, with STUN helping each side discover its public address. If no direct route works, traffic falls back to a TURN server that relays it. After that the media flows between peers, not through your signaling server.
These solve different problems. A webhook lets a service such as a payment provider or a code host tell your backend that something happened, by sending an HTTP request to a URL you registered. Nothing stays open, and no browser is involved.
A WebSocket keeps a live line to a user's browser. A common pattern uses both: a webhook tells your server an order was paid, and your server then pushes that news to the customer's open page over a WebSocket.
Plain HTTP is request and response: the client asks, the server answers, and GET responses can be cached by browsers and CDNs. It is the right default for pages, files and most API calls.
A WebSocket starts as an HTTP request, then switches protocols with a 101 response and stays open. Reach for it only when the server must send data the client did not ask for, or messages fly often enough that per-request overhead matters.
No. WebRTC has no built-in way for two browsers to find each other, so you still need a channel to exchange the setup messages. A WebSocket is the usual choice, though MDN notes that any transport works.
Yes. RTCPeerConnection.createDataChannel() opens a channel for text or binary data between peers, and its API is deliberately close to the WebSocket one, with send() and a message event.
A webhook is an HTTP request one server sends to a URL on another server when an event happens, then the connection ends. A WebSocket is a connection that stays open so a browser and server can keep talking.
For many small messages, usually yes, since the handshake happens once and each later message skips request headers. For loading a page or a file, ordinary HTTP is the right tool and benefits from caching.
Each connection is between two peers, so a full mesh grows quickly as people join. Many larger call products route media through a server that forwards streams instead, which sits outside the WebRTC browser API itself.
Related: SSE vs WebSocket, Webhook vs API, Stripe webhooks, HTTP status codes, GraphQL subscriptions.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.