Synchronous by Design: Why async/await in REST or gRPC Doesn’t Make Your System Asynchronous
Odumosu Matthew

Odumosu Matthew @iamcymentho

About: I'm currently deepening my expertise in Amazon Web Services (AWS) to strengthen my backend development skills. Given AWS's central role in modern infrastructure and cloud-native architecture, I see it

Location:
Lagos, Nigeria
Joined:
Jul 26, 2023

Synchronous by Design: Why async/await in REST or gRPC Doesn’t Make Your System Asynchronous

Publish Date: Jul 24
2 0

Modern .NET developers often lean on async/await to build performant APIs and services. But here's a critical truth:

Using async/await in a synchronous communication model doesn’t make your system asynchronous.

This misunderstanding is common in RESTful and gRPC-based architectures. It often leads to bottlenecks, false scalability assumptions, and overly tight coupling between services.

Let’s dive into the core of the issue what asynchronicity really means, how it's different from non-blocking code, and why REST/gRPC are inherently synchronous, even when wrapped in async/await.

🧠 1. Synchronous vs Asynchronous: Let’s Define Them Clearly

🔗 Synchronous Systems
In synchronous communication, the caller waits for a response from the callee before proceeding.

Example:

var user = await _userServiceClient.GetUserAsync(userId);

Enter fullscreen mode Exit fullscreen mode

Even though you're using await, the service cannot continue until it receives a response from the _userServiceClient. The thread may be released, but the logical flow is still request-response blocking.

⛓ Characteristics:

  • Tight temporal coupling

  • Immediate feedback required

  • Latency is part of business flow

  • Failure in downstream = upstream timeout/failure

🔄 Asynchronous Systems

In truly asynchronous systems, the caller doesn’t wait. It sends a message or triggers an event, and then continues. Processing happens independently, often triggered by an event or message queue.

Example:

await _messageBus.PublishAsync(new OrderPlacedEvent(orderId));

Enter fullscreen mode Exit fullscreen mode

There’s no response expected. The event is handed off to a queue, and processing happens elsewhere. If no consumer exists, it still doesn’t break the producer.

⚡ Characteristics:

  • Loose coupling

  • Message-driven

  • Can be processed later

  • Failures don’t immediately bubble back

🧩 2. async/await ≠ Asynchronous Architecture

Let’s make this crystal clear:
async/await is non-blocking, but REST or gRPC is still synchronous because it is fundamentally request-response.

⚠️ Misconception:

public async Task<IActionResult> GetOrders()
{
    var orders = await _orderService.GetOrdersAsync();
    return Ok(orders);
}
Enter fullscreen mode Exit fullscreen mode

Looks asynchronous. But it’s not an asynchronous system.

  • The controller waits for the service to respond.

  • The client waits for the controller to respond.

  • The whole call chain is temporally coupled, all services must be alive and responsive right now.

Even if it uses async/await internally, it’s synchronous by communication model.

🚫 Why REST/gRPC Stay Synchronous Even with async/await

REST:

  • HTTP 1.1/2 protocol

  • Follows strict request-response semantics

  • Timeout failures propagate up the chain

  • Cannot defer response naturally

gRPC:

  • Binary, faster, supports streaming

  • Still client waits for server

  • Streaming may reduce blocking, but still does not break coupling

Even with all the await, both REST and gRPC rely on:

  • Immediate network delivery

  • Live services

  • Predefined response structure

They cannot natively support:

  • Deferred processing

  • Unpredictable consumer availability

  • Retry logic with dead-letter queues

🏗 3. True Asynchronous Programming: Message Queues & Event-Driven Systems

Let’s now examine asynchronous programming at the system level, not just syntax.

🎯 Architecture

  • Client fires an event or enqueues a command

  • Server consumes it later, possibly in a different process, thread, or container

  • No waiting; temporal and spatial decoupling

🔁 Example in C#: Publish to a Queue

public async Task SubmitOrderAsync(Order order)
{
    var message = new ServiceBusMessage(JsonSerializer.Serialize(order));
    await _queueClient.SendMessageAsync(message);
}
Enter fullscreen mode Exit fullscreen mode

No OrderResponse needed. The message is stored, acknowledged, and processed later.

🧠 Why This Is Truly Asynchronous:

  • No round-trip waiting

  • Built-in retry, dead-letter, backpressure

  • System can scale horizontally

  • Consumers can process at their own pace

  • Supports resilience and eventual consistency

🔥 Real-World Analogy

Let’s compare two real-world systems:

Scenario Synchronous (REST/gRPC) Asynchronous (Messaging/Eventing)
Restaurant You order, wait at the counter You place an order, get a buzzer
Customer Action Blocks your time Continues your day
Server Dependency Must cook now Can fulfill when ready
Failure Customer leaves if delayed Buzzer system retries or notifies later

⚙️ C# System Comparison: Synchronous vs Asynchronous

🔗 REST Controller (Synchronous System)

[HttpPost]
public async Task<IActionResult> PlaceOrder(OrderDto order)
{
    var orderId = await _orderService.PlaceOrderAsync(order); // Waits here
    return Ok(orderId);
}

Enter fullscreen mode Exit fullscreen mode

🔁 Messaging Controller (Asynchronous System)

[HttpPost]
public async Task<IActionResult> PlaceOrder(OrderDto order)
{
    var orderId = Guid.NewGuid();
    var @event = new OrderPlacedEvent(orderId, order);
    await _messageBus.PublishAsync(@event);
    return Accepted(orderId);
}

Enter fullscreen mode Exit fullscreen mode

The request returns immediately, the actual work happens elsewhere.

🛠 Tools for Building Asynchronous Systems in .NET

Tool Purpose
MassTransit Message bus abstraction over RabbitMQ, Azure SB, etc.
Hangfire / Quartz.NET Delayed job scheduling
Azure Service Bus Durable messaging with retry, DLQ
Kafka High-throughput event streams
System.Threading.Channels In-memory backpressure pipeline
TPL Dataflow Actor-style pipelines and transformations

✅ Summary: Async ≠ Asynchronous System

Concept async/await Event-Driven Messaging
Thread release ✅ Yes ✅ Yes
Non-blocking
Decoupled systems
Fault isolation
Eventual consistency
Retry logic ❌ (manual) ✅ (built-in)
Scalable architecture ⚠️ Limited ✅ Horizontal scale ready

🧠 Final Thought

"async/await makes your code non-blocking, but only messaging and event-driven design make your architecture asynchronous."

As engineers, it's vital we understand this distinction. Use async/await to improve responsiveness. But when building resilient, scalable systems use queues, events, and asynchronous communication models to truly decouple and future-proof your system.

LinkedIn Account : LinkedIn
Twitter Account: Twitter
Credit: Graphics sourced from Dictionary.com

Comments 0 total

    Add comment