Microservices Communication: REST, gRPC, and Message Queues
Your microservices call each other via HTTP. One service goes down. Cascading failures bring everything down. Here is how to pick the right communication pattern.
Synchronous: REST and gRPC
REST: Simple, universal, human-readable. Best for CRUD operations and public APIs.
gRPC: Binary protocol (Protocol Buffers), type-safe, bidirectional streaming. Best for internal service-to-service calls where performance matters.
// REST: ~50ms per call, text-based
GET /api/users/123
// gRPC: ~5ms per call, binary, auto-generated clients
service UserService {
rpc GetUser (GetUserRequest) returns (User);
}
Asynchronous: Message Queues
For operations that do not need an immediate response:
// Producer: fire and forget
await queue.add("send-email", { to: user.email, template: "welcome" });
res.json({ status: "accepted" }); // Respond immediately
// Consumer: processes independently
worker.process("send-email", async (job) => {
await sendEmail(job.data);
});
When to Use What
| Pattern | Use When | Avoid When |
|---|---|---|
| REST | CRUD, public APIs, simple integrations | High-throughput internal calls |
| gRPC | Internal services, streaming, low latency | Browser clients, simple APIs |
| Message Queue | Async tasks, event-driven, decoupling | Need immediate response |
| Event Sourcing | Audit trails, complex workflows | Simple CRUD apps |
Key Principles
- Default to async (message queues) unless you need a synchronous response
- Use circuit breakers for all synchronous calls
- Make all operations idempotent
- Design for partial failure: one service being down should not crash everything
Part of my Production Backend Patterns series. Follow for more practical backend engineering.
Top comments (0)