gRPC vs. a message queue for service-to-service communication — when do you pick sync RPC vs. async messaging?
Both move data between services, but one blocks the caller waiting for an answer while the other doesn't care if it takes a minute.
LEARN BY DOINGPut your intuition to work3 steps
gRPC vs. a message queue for service-to-service communication — when do you pick sync RPC vs. async messaging?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Two different questions, not two flavors of the same tool
"Should this call be gRPC or should it go through a queue?" is really asking: does the caller need to know the outcome before it can proceed, within its own request's lifetime? If yes, you want synchronous RPC. If the caller can move on and something else should merely eventually happen as a result, you want asynchronous messaging. Everything else — latency, coupling, failure isolation, throughput — falls out of that one decision.
#gRPC: fast, typed, synchronous-by-default RPC
gRPC is a remote-procedure-call framework built on HTTP/2, using Protocol Buffers to define a strongly typed, versioned contract (a .proto file) from which client and server stubs are generated in whatever languages your services use. It supports four call shapes: unary (one request, one response — the common case), server-streaming, client-streaming, and full bidirectional streaming, and it has deadline propagation and cancellation built into the wire protocol, so a caller can tell an entire chain of downstream calls "you have 200ms, no more." The caller blocks (or, in async client code, awaits) until it gets a response or hits that deadline — the two services are coupled in time: if the callee is down or slow, the caller feels it immediately.
#Message queues: decoupled, buffered, eventually-processed
A message queue or event broker — Kafka, RabbitMQ, SQS/SNS — lets a producer publish a message and move on immediately, with zero expectation that the consumer processes it right away, or even that the consumer is running at that moment. This buys you three things simultaneously: decoupled availability (the producer's success doesn't depend on the consumer being up), natural load buffering (a traffic spike piles up as queue depth instead of as failed requests or exhausted connections), and fan-out (many independent consumers can subscribe to the same event without the producer knowing or caring who they are). The cost is that most brokers guarantee only at-least-once delivery, so every consumer must be idempotent, and the producer generally has no idea whether or when the message was actually handled.
#Comparing the two head to head
| Dimension | gRPC (sync RPC) | Message queue (async) |
|---|---|---|
| Coupling in time | Tight — caller waits for the callee | Loose — producer and consumer run independently |
| Failure visibility | Immediate, to the caller | Delayed or invisible to the producer; needs monitoring |
| Backpressure | Caller feels it directly (timeouts, errors) | Absorbed as queue depth, up to a limit |
| Fan-out | One caller, one callee (per call) | Natural — many consumers per topic |
| Delivery guarantee | Exactly the result of one call | Usually at-least-once; requires idempotent consumers |
| Ordering | N/A — it's a direct call | Depends on the broker (Kafka: per-partition order; SQS standard: no ordering) |
| Schema evolution | .proto contract, strict versioning discipline | Message schema, often paired with a schema registry |
| Best fit | User-facing request paths needing an immediate answer; internal reads where the caller cannot proceed without the result | Cross-service state propagation, workflows/sagas, audit/event log, decoupling producer uptime from consumer uptime |
#Failure modes push you toward different tooling
A synchronous RPC failure is a cascading-failure risk by construction: if service D is slow, and C calls it with gRPC and no timeout, C's own resources back up waiting on D, and that can propagate all the way to the edge (this is exactly the failure mode covered in the cascading-failures lesson in this module). Async messaging sidesteps that specific risk because the producer never blocks on the consumer at all — but it trades it for a different failure mode: a stuck or crashing consumer produces silent, growing lag rather than an immediate visible error, which is why consumer lag and dead-letter-queue depth need to be first-class metrics you alert on, not something you discover during an incident.
#The transactional outbox: making async publishing reliable
A subtle bug shows up the moment you try to combine "write to my own database" with "publish an event about it": if you write to the database and then publish to the broker as two separate operations, a crash between them either loses the event or, if you publish first, an event can go out for a database write that never committed. The transactional outbox pattern solves this by writing the event, as a row, into an outbox table in the very same database transaction as the business write, and having a separate relay process (or the database's own change-data-capture stream) read that outbox table and publish to the broker, retrying until it succeeds. That guarantees the event is published if and only if the transaction committed — this is table stakes for reliable event-driven microservices, and interviewers listen for it specifically.
#Guidance: most real systems use both
Treat this as a real decision per call, not a platform-wide choice — a healthy microservice architecture typically uses gRPC (or REST) for synchronous reads and any call where the caller genuinely cannot proceed without an answer, and an event bus for propagating state changes to every service that cares about them. Seeing both technologies inside the same system is not indecision, it's the normal shape of a mature architecture.
🧠 Check your understanding
0/2 · 0/2 answered1. A checkout service needs to know whether a payment succeeded before it can show the customer a confirmation page. Which communication style fits best?
2. What problem does the transactional outbox pattern solve?