Back to Blog
Jun 1, 20264 min readOnuzulike Anthony

Service Communication: REST, gRPC, and Message Queues

When to use each inter-service communication pattern, what it costs you, and the developer ergonomics trade-offs nobody talks about.

ArchitectureArchitectureDesignAPI

The communication pattern between services shapes your team's day-to-day development experience more than almost any other architectural decision. It determines how you handle schema evolution, how you debug production issues, and how much coupling you have between teams.

REST over HTTP

REST is the default for good reason. Every language has an HTTP client. Every proxy, load balancer, and CDN understands HTTP. You can test it with curl.

Strengths:

  • Universal tooling and client support
  • Human-readable requests and responses
  • Works natively in browsers
  • Schema is optional (can be added later with OpenAPI)
  • Caching via HTTP cache semantics is built in

Weaknesses:

  • No enforced schema without additional tooling
  • Text-based serialization (JSON) is slower than binary protocols for high-throughput internal calls
  • No native streaming support in HTTP/1.1 (HTTP/2 helps)
  • Service discovery and load balancing need to be handled externally

For services that are user-facing or that different teams consume at different rates, REST is almost always the right choice. The friction of schema enforcement is lower than the friction of gRPC setup for teams not already using it.

gRPC

gRPC is a binary RPC framework built on HTTP/2, using Protocol Buffers for schema definition and serialization.

protobuf
// user.proto
service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc StreamUserEvents (UserEventRequest) returns (stream UserEvent);
}
 
message GetUserRequest { string userId = 1; }
message User {
  string id = 1;
  string email = 2;
  int64 createdAt = 3;
}

Strengths:

  • Schema-first: the .proto file is the contract; client and server code is generated from it
  • Binary serialization is ~5-7× faster and smaller than JSON for equivalent data
  • Native streaming (server, client, and bidirectional)
  • Strongly typed clients generated for every supported language
  • HTTP/2 multiplexing: multiple RPC calls over one connection

Weaknesses:

  • Not human-readable for debugging (requires tooling like grpcurl or Postman gRPC)
  • Not supported natively in browsers (requires gRPC-Web + proxy, or gRPC-Web shim)
  • Proto files require a shared build system to keep in sync across services
  • Schema evolution requires careful field numbering discipline (never reuse field numbers)

gRPC shines for: high-throughput internal service calls (10k+ RPS), streaming data, polyglot environments where generated clients save significant work.

It's overkill for: services handling under 1k RPS, browser-facing APIs, small teams where schema synchronization overhead outweighs the performance benefit.

Message Queues

Message queues (RabbitMQ, Kafka, Cloudflare Queues, SQS) decouple the producer and consumer in time. The producer publishes a message and continues; the consumer processes it at its own pace.

ts
// Producer: NodWatch's recommendation refresh trigger
await env.REGEN_QUEUE.send({ userId, reason: 'watchlist_updated' });
 
// Consumer: Worker processes regeneration
export default {
  async queue(batch: MessageBatch) {
    for (const message of batch.messages) {
      const { userId } = message.body;
      await recomputeRecommendations(userId);
      message.ack();
    }
  }
};

Strengths:

  • Temporal decoupling: producer and consumer don't need to be up simultaneously
  • Natural load leveling: the queue absorbs traffic spikes
  • Built-in retry and dead-letter queues for failed messages
  • Multiple consumers can process from the same queue independently

Weaknesses:

  • Eventual consistency: the downstream action happens after the API returns
  • Harder to debug: a failure in the consumer is invisible to the original caller
  • Message ordering is complex (most queues guarantee at-least-once, not exactly-once)
  • Operational overhead of managing queue infrastructure

The Developer Ergonomics Nobody Talks About

The performance numbers matter less than you'd expect at typical application scale. What matters more is: how easy is it to trace a request when something goes wrong?

With REST, you have a request ID in the logs, a response code, and a response body. The full interaction is in one place.

With gRPC, you need your observability stack to understand protobuf and decode the binary frames. grpcurl helps locally; production debugging requires investment in tracing.

With message queues, a failure in consumer processing is invisible to the original caller. You need dead-letter queues, consumer error logging, and alerting to know when messages are failing.

Correlation IDs (propagated through all calls and logged everywhere) are necessary for debugging any inter-service communication at scale, but become non-optional when you mix synchronous and async calls.

A Practical Starting Point

Start all internal service communication with REST. When a specific performance measurement shows that JSON serialization or per-connection overhead is a bottleneck, migrate that specific call to gRPC. When a specific service interaction naturally decouples (notification sending, cache warming, analytics), introduce a queue for that flow.

Don't architect for "eventual microservices scale" from the start. The complexity cost is paid immediately; the performance benefit is hypothetical.