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:
-
Performance: Is HTTP significantly faster for lightweight queries?
-
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 testingk6-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.