Ch. 10 · Microservices

Microservices: Synchronous Calls Versus Messaging

When a service should call another and wait, when it should publish an event instead, and what availability math and an email outage show.

~6 min readbeginnerupdated Oct 7, 2026

Every interaction between two services is one of two shapes. In a synchronous call, service A sends a request to B and waits for the answer before continuing: HTTP, gRPC. In asynchronous messaging, A publishes a message to a broker and carries on; B processes it whenever it can: Kafka, RabbitMQ, SQS.

Neither is better in general. Interviewers ask about this to see whether you can choose per interaction and explain the trade-off, rather than declaring that “event-driven is more scalable” or “REST is simpler”. The deciding question is usually simple: does the caller need the result to continue?

What a synchronous call ties together

When checkout calls the payment service and waits, checkout’s success now depends on payment being running, reachable and fast at that exact moment. That’s temporal coupling: both services must be available at the same time. Each synchronous dependency adds its latency to the caller’s, and its downtime too.

The availability side compounds quickly. If every service in a chain is up 99.9% of the time and failures are independent, the chance that all of them are up at once is the product:

for n in (1, 3, 5, 10):
    a = 0.999 ** n
    print(f"{n:2} services at 99.9% each, all called synchronously: {a:.4%} "
          f"(~{(1 - a) * 30 * 24 * 60:.0f} min of failure per 30 days)")
python
 1 services at 99.9% each, all called synchronously: 99.9000% (~43 min of failure per 30 days)
 3 services at 99.9% each, all called synchronously: 99.7003% (~129 min of failure per 30 days)
 5 services at 99.9% each, all called synchronously: 99.5010% (~216 min of failure per 30 days)
10 services at 99.9% each, all called synchronously: 99.0045% (~430 min of failure per 30 days)
Text

A request that synchronously touches ten services fails roughly ten times as often as one that touches one. Real failures aren’t independent (a shared database or network outage takes several down together), so treat this as an intuition rather than a forecast. The direction is right, though: long synchronous chains make the whole system as fragile as the sum of its parts.

The email outage, both ways

A concrete case makes the choice clearer. After checkout, the customer should get a confirmation email. Suppose the email service is down for 10 minutes and one order arrives per minute over 20 minutes.

email_up = lambda minute: not (3 <= minute < 13)

sync_ok = sum(1 for m in range(20) if email_up(m))
print(f"\nsync:  {sync_ok}/20 checkouts succeeded (checkout waits for the email service)")

queue, sent, accepted = deque(), [], 0
for m in range(20):
    accepted += 1
    queue.append(f"order-{m}")          # OrderPlaced event; checkout returns immediately
    while queue and email_up(m):        # the email consumer drains whatever it can
        sent.append((queue.popleft(), m))
python

Output from Python 3:

sync:  10/20 checkouts succeeded (checkout waits for the email service)
async: 20/20 checkouts succeeded; 20 emails sent, 10 of them late (max delay 10 min)
Text

With a synchronous call, half the customers couldn’t buy anything for 10 minutes because a confirmation email couldn’t be sent. That’s a bad trade: the business would much rather take the order and send the email later. With an OrderPlaced event, checkout never noticed the outage. The broker held the messages, and when the email service recovered, it caught up. Ten customers got their email up to 10 minutes late, which almost nobody will mind.

The lesson isn’t “always use messaging”. It’s that the email was never a precondition for completing checkout. It’s a consequence of checkout. Consequences are natural candidates for events.

When a synchronous call is the right choice

Some interactions really do need an answer before the caller can continue:

  • The response depends on it. Showing a product page needs the price. Validating a login needs the identity provider’s answer.
  • A decision depends on it. Accepting an order for an item with limited stock needs the inventory service’s confirmation that a unit is reserved.
  • The user is waiting for a result they asked for. Searching, fetching an account balance, loading a dashboard.

Forcing these through messaging tends to produce request-reply over a broker: publish a request message, then wait on a reply queue with a correlation id. It’s still synchronous from the user’s point of view, just with more moving parts. Use HTTP or gRPC with a timeout, a retry policy and a circuit breaker instead.

When messaging is the right choice

Messaging fits when the producer needs something to happen but doesn’t need the result to continue:

  • Side effects of a completed action. Send an email, update a search index, award loyalty points, write an audit record.
  • Fan-out. Several services react to one fact. With events, the order service publishes OrderPlaced once and doesn’t need to know who’s listening. With calls, it would need to call each one and handle each one failing.
  • Long-running work. Generating a report, transcoding a video. Accept the request, return 202 Accepted with a status URL, and do the work from a queue.
  • Absorbing load spikes. A queue lets consumers process at their own pace while producers keep accepting work.

What messaging costs

Candidates who only list advantages get pushed on this, so it’s worth saying unprompted:

  • Eventual consistency. After checkout, the loyalty balance won’t show the new points for a moment. UIs and other services must cope with that.
  • Duplicates and ordering. Most brokers deliver at least once. Consumers must deduplicate, and must not assume events arrive in order unless partitioning guarantees it.
  • Reliable publishing. Saving the order and publishing the event are two writes; an outbox is needed so a crash between them doesn’t lose the event.
  • Harder debugging. A request now spans a producer, a broker and consumers that run later. Distributed tracing across message headers and dead-letter monitoring become necessary, not optional.
  • Errors can’t go back to the user directly. If the loyalty consumer rejects an event, the customer has already seen “order confirmed”. Failures have to be handled by retries, dead-letter queues and compensating actions.
  • Another system to run. The broker needs capacity planning, monitoring and upgrades.

Mixing them in one flow

Real flows combine both. A typical checkout: synchronously reserve stock and authorise the payment (the order can’t be confirmed without them), then commit the order and an outbox event in one transaction, return “order confirmed” to the customer, and let email, loyalty, analytics and warehouse picking react to the event. The synchronous part is kept as short as possible: only what’s needed to give the customer a truthful answer.

Interview practice: the slow order confirmation

“Order placement makes synchronous HTTP calls to inventory, payments, fraud scoring, loyalty, email and analytics, one after another. p99 latency is 2.8 seconds, and any of the six being down blocks orders. How would you redesign it?”

Answer

I’d sort the six calls by whether the order needs their answer before it can be confirmed. Inventory (is there a unit for this customer?) and payments (is the money authorised?) clearly do. Fraud scoring probably does too, though it could become asynchronous for low-value orders with a review step for flagged ones. Loyalty, email and analytics don’t: they’re consequences of an order, not conditions for it.

So the synchronous path becomes inventory, fraud and payment, each with a tight timeout and a defined fallback, and where possible inventory and fraud run in parallel since they don’t depend on each other. Then the order and an OrderPlaced event are written in one transaction using an outbox, and the API returns. Loyalty, email and analytics consume the event independently, with deduplication on the event id. Latency should drop to roughly the slowest of the remaining critical calls plus the database write, and an outage in loyalty, email or analytics no longer stops anyone from buying. I’d mention the costs too: loyalty points and confirmation emails become eventually consistent, and the team needs consumer lag and dead-letter monitoring for the new consumers.

Further reading

More in Microservices

read ✓Microservices · easy

Microservice Consumers and Duplicate Events

Why at-least-once delivery hands a consumer the same event twice, and how a processed-message table in one transaction stops a double credit.

~6 min readread →
esc