APIs & Auth

REST, GraphQL, gRPC, WebSockets, or SSE? Choosing an API Protocol

"Which API style should we use?" gets argued as if one answer were correct. In practice, each protocol makes a different set of trade-offs, and many production systems use three or four side by side: REST for the public API, gRPC between internal services, and WebSockets or Server-Sent Events for live updates in the UI.

This guide compares the options on the dimensions that actually drive the decision: who the clients are, whether data flows one way or both ways, how much caching matters, and how much tooling friction you can tolerate.

At a glance

RESTGraphQLgRPCWebSocketSSE
TransportHTTPHTTP (usually one POST endpoint)HTTP/2Upgraded HTTP connectionHTTP (long-lived response)
PayloadUsually JSONJSONProtocol Buffers (binary)Any text or binaryText events
DirectionRequest → responseRequest → response (+ subscriptions)Unary and streaming, both waysFull duplexServer → client only
HTTP cachingExcellentHardNone built inN/AN/A
Browser supportNativeNativeNeeds gRPC-Web + proxyNativeNative
ContractOptional (OpenAPI)Required (schema)Required (.proto)NoneNone

REST: the default for good reasons

REST models your API as resources, such as /users/42 and /users/42/orders, manipulated with standard HTTP methods. Its biggest strength is that it goes with the grain of HTTP. Every proxy, CDN, browser cache, and monitoring tool already understands GET, status codes, and Cache-Control.

Choose REST when you're building a public or partner API, when responses are cacheable, or when you want the lowest barrier for consumers: anyone with curl can call it.

Watch out for over-fetching, where endpoints return more than a screen needs, and under-fetching, where one screen needs five round trips. An OpenAPI description goes a long way toward giving REST the discoverability that schema-first protocols have built in.

GraphQL: the client chooses the shape

GraphQL exposes a typed schema and lets the client send a query describing exactly the fields it wants, across related objects, in one request:

query {
  user(id: "42") {
    name
    orders(last: 3) { id total status }
  }
}

Choose GraphQL when you have many different clients, such as web, iOS, Android, and partners, with different data needs, and a frontend team that iterates faster than the backend can add bespoke endpoints.

Watch out for:

  • Caching. Queries typically go as POSTs to a single URL, so HTTP-level caching doesn't apply without extra work, such as persisted queries sent over GET.
  • N+1 queries. A naive resolver for orders runs once per user. Batching, via the DataLoader pattern or similar, is effectively mandatory.
  • Query cost. Clients can write deeply nested, expensive queries. Production servers need depth or complexity limits.
  • Errors. GraphQL commonly returns HTTP 200 with an errors array, so monitoring that looks only at status codes will miss failures.

gRPC: fast, typed, service-to-service

gRPC defines services and messages in a .proto file, generates client and server code in many languages, and sends compact binary Protocol Buffers over HTTP/2:

service OrderService {
  rpc GetOrder (GetOrderRequest) returns (Order);
  rpc WatchOrders (WatchRequest) returns (stream OrderEvent);
}

Choose gRPC when you control both ends, typically for internal microservices, and care about latency, payload size, strict contracts, or streaming. Generated clients remove a whole category of "the field was renamed" bugs.

Watch out for browser support. Browsers don't expose the HTTP/2 framing gRPC needs, so web clients require gRPC-Web and a translating proxy. Binary payloads are also harder to inspect by eye than JSON, so you'll want a client that understands your .proto files when debugging.

WebSockets: full-duplex, long-lived connections

A WebSocket starts as an HTTP request, upgrades, and then stays open, letting either side send messages at any time with very little overhead per message.

Choose WebSockets when both sides need to send frequently and with low latency: chat, multiplayer and collaborative editing, live trading screens.

Watch out for everything HTTP used to do for you. You'll need your own message format, your own request/response correlation if you need it, your own reconnection and backoff logic, and your own authentication strategy, since browsers can't set arbitrary headers on the WebSocket handshake. Load balancers must also support long-lived connections. Libraries like Socket.IO add reconnection, rooms, and fallbacks on top, but they use their own protocol, so a plain WebSocket client can't talk to a Socket.IO server.

Server-Sent Events: the underrated option

SSE is a plain HTTP response that never ends. The server keeps writing data: lines as events happen:

event: order-updated
data: {"id": 812, "status": "shipped"}

Browsers consume it with the built-in EventSource API, which reconnects automatically and can resume from the last event ID.

Choose SSE when updates flow only from server to client: notifications, progress bars, live dashboards, and streaming AI responses token by token. It's far simpler to operate than WebSockets because it's just HTTP. Client-to-server messages can go over ordinary requests.

Watch out for connection limits under HTTP/1.1, where browsers cap open connections per domain at around six. Under HTTP/2 this largely goes away.

Beyond the big five

Some domains have protocols built for their specific constraints:

  • MQTT — lightweight publish/subscribe for IoT devices on unreliable networks, with quality-of-service levels for delivery guarantees.
  • SOAP — XML envelopes with WSDL contracts. Rarely chosen for new work, but still common in banking, government, and enterprise integrations you may have to call.
  • Webhooks — not a protocol so much as a pattern: you expose a REST endpoint and another service calls it when something happens. It's the usual way SaaS products notify you of events.

A decision checklist

  1. Who calls this API? External developers point toward REST, perhaps with GraphQL. Your own backend services point toward gRPC.
  2. Does data need to flow server → client unprompted? If it's one-way, use SSE. If both directions are frequent, use WebSockets.
  3. Is caching important? REST makes it nearly free.
  4. Do clients need very different views of the same data? GraphQL earns its complexity here.
  5. Is latency or payload size critical between services? gRPC.

Whichever you choose, you'll need to test it — and that's much easier with a single client that speaks all of them than with a separate tool per protocol. Once requests are flowing, getting status codes right is the next thing that separates a pleasant API from a frustrating one.

Try this in Voyager

A local-first API client for REST, GraphQL, WebSocket, gRPC, and 10 more protocols.

Keep reading

More APIs & Auth guides