TypeScript

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

gRPC vs WebSockets in Node.js: Choosing the Right Real-Time Transport

"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:

Protobuf
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:

Typescript
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:

Typescript
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 .proto is a single source of truth that generates Go, Rust, Python, and TypeScript clients that cannot silently disagree.

Trade-offs at a glance

gRPCWebSockets
Schema / contractEnforced (protobuf)You define it
Browser supportVia gRPC-Web + proxyNative
StreamingFour typed modesOne raw message stream
BackpressureBuilt in (HTTP/2 flow control)Do it yourself
Best fitInternal service-to-serviceBrowser 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.

Go further
Live cohort on Maven

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 cohort

Keep reading