Messaging vs HTTP in .NET Microservices: A Practical Benchmark


Introduction

In microservices architecture, it's generally considered a bad practice to make synchronous request/response calls across service boundaries — especially using HTTP. The ideal model is to design services to be autonomous, decoupled, and event-driven.

However, in reality, there are times when one service needs to fetch data from another — and when that happens, most developers instinctively reach for a direct HTTP call. It's simple, familiar, and it seems fast and efficient.

On the other hand, messaging systems like Azure Service Bus or RabbitMQ are often recommended when handling commands or state-changing operations — because they offer reliability, durability, retry policies, and built-in backpressure handling. In those scenarios, the benefits are obvious.

But what about read-only queries?
What about something as simple as GET /orders?

At first glance, HTTP seems like the obvious choice for a query. After all, it's just fetching data — why add the overhead of a message broker?

This article challenges that assumption. Is HTTP truly the better option for queries in microservices? Or do messaging patterns offer hidden benefits — even in read-only scenarios?

Let’s find out by applying pressure.

You can find the full source code, scripts, and reports for this benchmark on GitHub:
GeorgopoulosGiannis/HttpVsMessaging.Benchmarks


What We’re Testing

We’ll be benchmarking two different implementations of a simple GET /orders query in a microservices setup:

  • One service calls another over HTTP using HttpClient

  • The same request is made using MassTransit with RabbitMQ

We want to answer two key questions:

  1. Performance: Is HTTP significantly faster for lightweight queries?

  2. Reliability under load: What happens when the system is slow or under pressure?


Technologies Used

  • .NET 9
  • MassTransit + RabbitMQ (for messaging)
  • Plain HTTP with HttpClient
  • k6 for load testing
  • k6-reporter for HTML reports

Why MassTransit?

For the messaging transport in this benchmark, I used RabbitMQ along with MassTransit. MassTransit is mature, stable, and widely used. It supports key enterprise messaging patterns like publish/subscribe, routing, sagas, and request/response.

Using MassTransit means we didn’t have to reinvent retry logic, dead-lettering, or backpressure handling. That’s the power of messaging: it comes with the patterns baked in.

Services Involved

Service Description
DirectHttp.Service Exposes GET /orders over HTTP
DirectHttp.Client Uses HttpClient to call DirectHttp.Service
Messaging.Worker MassTransit consumer that handles GetOrders
Messaging.Client Sends GetOrders via MassTransit, exposes HTTP for testing

All services run in Docker Compose to simulate realistic network boundaries.


Benchmark Scenarios

We narrowed the benchmark down to two essential scenarios:

Scenario Virtual Users (VUs) Delay in Handler Purpose
baseline 50 500ms Normal query in a slow backend
highload 200+ 500ms High concurrency, slow backend

By simulating a 500ms processing delay, we replicate a common real-world condition: a service that is healthy but slow (e.g., due to I/O, database latency, etc.).

In both cases, we compare how HTTP and messaging behave in terms of throughput, latency, and most importantly — failure rate.


Avoiding Startup Failures

In earlier tests, messaging scenarios failed briefly at the beginning — not because the system was under pressure, but because RabbitMQ needed a moment to fully initialize its queues and bindings.

To make the benchmark fair, we added a ramp-up phase using ramping-vus in k6. This gradually increases the load instead of starting at full blast.

Benefits of ramping up:

  • Allows MassTransit and RabbitMQ to fully initialize

  • Prevents startup spikes from skewing results

  • Makes the test more representative of real-world behavior


How the Benchmarks Were Run

  • All services run inside Docker Compose, including RabbitMQ

  • run-benchmarks.sh:

    • Builds and starts the services

    • Runs each benchmark scenario using k6

    • Saves structured logs and summary metrics in benchmarkResults/

  • Metrics captured:

    • Requests per second

    • P95 latency

    • Failure count

    • Full HTML reports for each run

You can inspect or reproduce the tests by running the benchmark script in the repo.


Results & Observations

Step 1: Baseline — HTTP is Faster, as Expected

To start, I ran both the HTTP and messaging-based implementations under a light load — a gradual ramp-up to 50 virtual users (VUs) over 25 seconds. Both implementations introduced a simulated 500ms delay in the handler to mimic a slow backend service.

At this point, the HTTP server had a configured limit of 100 concurrent connections using the KestrelServerOptions.Limits.MaxConcurrentConnections setting — which is a common production safeguard to avoid thread exhaustion.

Here’s how the baseline performed:

Scenario Requests/sec P95 Latency Failures Report
baseline_http 48.35/s 513.3 ms 0 baseline_http
baseline_messaging 25.09/s 1.52 s 0 baseline_messaging

As expected, HTTP was almost 2× faster than messaging.

  • Both systems were stable with zero failures.

  • HTTP returned responses just above the simulated delay (~500ms).

  • Messaging was slower due to the internal message handling and broker hop via RabbitMQ and MassTransit.

This confirmed the general assumption:

When the system is healthy and lightly loaded, HTTP is faster and works great — especially for simple queries.

But things change under stress.

Step 2: What Happens Under Load?

Next, I ramped up to 200 virtual users to simulate traffic spikes — still using the same 500ms artificial delay in the backend.

This is where things got interesting:

Scenario Requests/sec P95 Latency Failures Report
highload_http 977.26/s 575.58 ms 24,836 highload_http
highload_messaging 30.61/s 6.39 s 0 highload_messaging

At first glance, HTTP looks great — almost 1,000 requests per second. But look again:
almost 25,000 requests failed.

These weren’t logical errors or bad data. They were infrastructure failures:

  • Connection pool exhaustion

  • Timeouts

  • Refused or dropped connections

Meanwhile, messaging was slower — but every message made it through. Zero failures.

The messaging system simply queued incoming requests, letting the worker handle them as it could.
The HTTP server, on the other hand, hit its concurrency cap and started failing under pressure.

Messaging degrades gracefully.
HTTP degrades violently.


Conclusion: Messaging Isn't Always About Speed — It's About Control

When you’re only looking at performance, HTTP wins.

But as soon as resilience enters the conversation — whether that’s:

  • A slow database,

  • A spike in traffic,

  • Or a thread pool running hot…

messaging shows its value.

You get:

  • Built-in backpressure

  • Automatic queuing

  • Durable retries

  • Decoupling between services

And the most important thing: your request doesn’t get lost.


Messaging might not be your first instinct for queries. But if your system can get slow — and it probably will — messaging might be exactly what keeps it stable.