gRPC vs WebSockets in Node.js: Choosing the Right Real-Time Transport
Websockets get the job done for real-time communication. But gRPC does it faster, with stronger contracts, and less room for mistakes.
5 May 2024

"Should I use gRPC or WebSockets for this?" is a question I get a lot, usually phrased as if one is simply better. It is the wrong framing. They overlap, but they solve different problems, and the right answer depends on who your client is and what "real-time" means for you.
Here is the honest comparison, with the Node.js code for both.
What each one actually is
WebSockets give you a single, long-lived, bidirectional TCP connection between a client and a server. Once the HTTP upgrade completes, either side can push raw messages at any time. There is no schema, no method contract, no code generation. You send bytes or strings and agree on the meaning yourself. It is browser-native, which is its biggest advantage.
gRPC is a Remote Procedure Call framework: you define a service and its messages in a .proto file, and codegen produces typed clients and servers. It runs over HTTP/2 and serializes with Protocol Buffers, a compact binary format. It supports four call types: unary (request/response), server-streaming, client-streaming, and bidirectional streaming.
The one-line distinction: WebSockets are a transport you build a protocol on top of. gRPC is a protocol with the transport, schema, and codegen already decided for you.
The HTTP/2 foundation
gRPC's strengths come from HTTP/2: multiple concurrent streams over one connection (multiplexing), header compression, and per-stream flow control that gives you real backpressure. A slow consumer naturally slows the producer instead of drowning in an unbounded buffer. With raw WebSockets you get one message stream and have to implement backpressure and framing yourself.
gRPC in Node.js
Define the contract once:
syntax = "proto3";
package chat;
service Feed {
rpc Subscribe(SubscribeRequest) returns (stream Event);
}
message SubscribeRequest { string channel = 1; }
message Event { string id = 1; string payload = 2; }
Then implement the server with @grpc/grpc-js and @grpc/proto-loader:
import { loadPackageDefinition, Server, ServerCredentials } from "@grpc/grpc-js";
import { loadSync } from "@grpc/proto-loader";
const pkg = loadPackageDefinition(loadSync("feed.proto")) as any;
const server = new Server();
server.addService(pkg.chat.Feed.service, {
Subscribe(call: any) {
const { channel } = call.request;
const timer = setInterval(() => {
call.write({ id: crypto.randomUUID(), payload: `tick on ${channel}` });
}, 1000);
call.on("cancel", () => clearInterval(timer));
},
});
server.bindAsync("0.0.0.0:50051", ServerCredentials.createInsecure(), () => {
console.log("gRPC feed on :50051");
});
The client is generated from the same proto, so the method name, request shape, and streamed Event type are all typed. Change the contract and both sides fail to compile until they agree. That is the property you are really buying.
WebSockets in Node.js
The equivalent with ws is smaller, and untyped:
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket) => {
const timer = setInterval(() => {
socket.send(JSON.stringify({ id: crypto.randomUUID(), payload: "tick" }));
}, 1000);
socket.on("close", () => clearInterval(timer));
});
No proto, no codegen, no contract. For a simple broadcast that is a feature, not a gap. You also get to talk to it directly from a browser with the built-in WebSocket, no proxy required.
The decision that actually matters: who is your client?
- A browser. Native gRPC does not run in browsers, it needs HTTP/2 trailers the fetch API does not expose. You either use WebSockets or use gRPC-Web, which requires a proxy (Envoy or a Node middleware) to translate. For a browser-to-server realtime feature, WebSockets are usually the pragmatic choice.
- Service to service, inside your own network. This is where gRPC wins clearly: the typed contract, generated clients in every language, streaming semantics, and binary payloads pay off across a fleet of services.
- Fan-out, chat, presence, pub/sub. WebSockets plus a message broker is simpler and the ecosystem (Socket.IO and friends) is built for it.
- Polyglot backends that must not drift. gRPC's
.protois a single source of truth that generates Go, Rust, Python, and TypeScript clients that cannot silently disagree.
Trade-offs at a glance
| gRPC | WebSockets | |
|---|---|---|
| Schema / contract | Enforced (protobuf) | You define it |
| Browser support | Via gRPC-Web + proxy | Native |
| Streaming | Four typed modes | One raw message stream |
| Backpressure | Built in (HTTP/2 flow control) | Do it yourself |
| Best fit | Internal service-to-service | Browser realtime, fan-out |
Wrapping up
gRPC is not a "better WebSocket." It is a contract-first RPC framework that happens to stream, and it shines for internal service-to-service communication. WebSockets are a raw, browser-native, bidirectional pipe, and they shine when the client is a browser or the pattern is broadcast. Pick by asking who connects and whether you need an enforced schema, not by asking which is newer.
From Senior to Staff: Master the Architecture Skills That Get You Promoted
Go from shaky in design reviews to the engineer everyone trusts to architect the hard stuff.
View the live cohortKeep reading
- The Frontend Toolchain Is Now Written in Rust and Go. What That Means for You
- oxlint vs ESLint: Why I Replaced ESLint with oxlint
- SystemJS Is Dead. Native ESM Finally Won.
- Replace Axios with a Simple Custom Fetch Wrapper (Production-Ready Guide)
- Stop Shipping ChatGPT Wrappers. Ship an Agent in TypeScript, or Don't Bother.
- What Developers Are Saying About React 19: Pros and Cons