Choosing the right communication style between microservices is one of the most important architectural decisions you will make.
Get it wrong, and you end up with a fragile system where one failing service brings everything down.
Get it right, and your services stay independent, scalable, and resilient.
In distributed systems, there are two fundamental ways services talk to each other: synchronous and asynchronous communication.
Each comes with trade-offs in latency, coupling, reliability, and complexity.
Throughout my career, I have designed and built distributed systems using both approaches.
In this post, I will break down how each communication style works, when to use each one, and how real systems often combine both to achieve the best results.
In this post, we will explore:
- How Microservices Communicate
- Synchronous Communication: HTTP and gRPC
- Asynchronous Communication: Message Brokers and Event Streaming
- Synchronous vs Asynchronous: Deep Comparison
- Hybrid Communication: Mixing Sync and Async in One Flow
- How to Choose: Decision Checklist
- Summary
Let's dive in.
👉 Read original article on my newsletter: https://antondevtips.com/blog/synchronous-vs-asynchronous-communication-in-microservices
How Microservices Communicate
In a Modular Monolith application, components call each other through in-process method calls.
It's fast, simple, and reliable.
But when you break a Modular Monolith into microservices, those calls become network calls.
And network calls introduce latency, failures, and a whole new set of challenges.
There are two fundamental approaches for microservices communication:
Synchronous communication: the caller sends a request and waits for a response before continuing.
The caller is blocked until the response arrives or a timeout occurs.
Think of it like a phone call - you dial, the other person picks up, you talk, and you wait for an answer before continuing the conversation.
Asynchronous communication: the caller sends a message and moves on without waiting for an immediate response.
The message is processed later by the receiving service at its own pace.
Think of it like sending an email - you send it and continue with your work, without waiting for the recipient to read and reply.
These two styles lead to fundamentally different system behaviors in terms of coupling, resilience, and scalability.
Now let's explore each communication style in detail.
Synchronous Communication: HTTP and gRPC
Synchronous communication is the most intuitive way for services to talk.
Service A calls Service B, waits for the result, and then continues its work.
The two most popular protocols for synchronous communication in .NET are HTTP (REST) and gRPC.
You can also use HotChocolate GraphQL Strawberry Shake for HTTP/REST communication. But this is a pretty niche approach. If you want to learn more about HotChocolate GraphQL, see this article.
HTTP/REST
HTTP-based REST APIs are the most common way microservices communicate synchronously.
They are simple to build, easy to debug, and supported by virtually every language and framework.
Here is a typical example. The Booking Service needs to check room availability from the Property Service before confirming a reservation:
public class PropertyServiceClient
{
private readonly HttpClient _httpClient;
public PropertyServiceClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<RoomAvailability?> CheckAvailabilityAsync(
int propertyId, DateOnly checkIn, DateOnly checkOut)
{
var response = await _httpClient.GetAsync(
$"/api/properties/{propertyId}/availability?checkIn={checkIn}&checkOut={checkOut}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<RoomAvailability>();
}
}
HTTP/REST works well when:
- You need a simple request-response interaction
- The client needs data immediately to continue
- You are building public APIs
- You want broad compatibility across languages and platforms
The main downside is temporal coupling - the caller must wait for the receiver to respond.
If the Property Service is slow or down, the Booking Service is stuck.
gRPC
gRPC is a high-performance RPC (Remote Procedure Call) framework built on HTTP/2.
It uses Protocol Buffers (Protobuf) for serialization, which is significantly faster and smaller than JSON.
gRPC shines in service-to-service communication where performance matters.
Let's explore an example: how to check the room availability with gRPC.
First, define the service contract in a .proto file:
syntax = "proto3";
option csharp_namespace = "BookingPlatform.Protos";
service PropertyService {
rpc CheckAvailability (AvailabilityRequest) returns (AvailabilityResponse);
}
message AvailabilityRequest {
int32 property_id = 1;
string check_in = 2;
string check_out = 3;
}
message AvailabilityResponse {
bool is_available = 1;
double price_per_night = 2;
string currency = 3;
}
Then call it from the Booking Service:
public class PropertyGrpcClient
{
private readonly PropertyService.PropertyServiceClient _client;
public PropertyGrpcClient(PropertyService.PropertyServiceClient client)
{
_client = client;
}
public async Task<AvailabilityResponse> CheckAvailabilityAsync(
int propertyId, string checkIn, string checkOut)
{
var request = new AvailabilityRequest
{
PropertyId = propertyId,
CheckIn = checkIn,
CheckOut = checkOut
};
return await _client.CheckAvailabilityAsync(request);
}
}
gRPC works well when:
- You need low-latency, high-throughput communication between internal services
- You have strict performance requirements
- You want strongly typed contracts between services
- You need streaming capabilities (server streaming, client streaming, or bidirectional)
gRPC is not ideal for browser-to-service communication (though gRPC-Web exists as a workaround) or when you need human-readable payloads for debugging.
Handling Failures in Synchronous Communication
The biggest risk with synchronous communication is cascading failures.
When Service A calls Service B, and Service B calls Service C - if Service C is down, all three services are affected.
To mitigate this, you should implement resilience patterns:
Retries with backoff - retry failed requests with increasing delays between attempts:
builder.Services.AddHttpClient<PropertyServiceClient>(client =>
{
client.BaseAddress = new Uri("https://property-service:5001");
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.Retry.Delay = TimeSpan.FromMilliseconds(500);
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
options.CircuitBreaker.FailureRatio = 0.9;
options.CircuitBreaker.MinimumThroughput = 5;
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(2);
});
The AddStandardResilienceHandler from Microsoft.Extensions.Http.Resilience gives you retries, circuit breaker, and timeouts in a single configuration.
Circuit Breaker - stops calling a failing service to give it time to recover.
When a service fails repeatedly, the circuit "opens," and all subsequent requests fail immediately instead of waiting for a timeout.
After a cooldown period, the circuit "half-opens" to test if the service has recovered.
These patterns don't eliminate the problem of synchronous coupling.
They reduce the impact.
If you need true independence between services, you need asynchronous communication.
👉 Read original article on my newsletter: https://antondevtips.com/blog/synchronous-vs-asynchronous-communication-in-microservices
Top comments (0)