The Race Car vs. The Go-Kart: Why My Fastest Code Was Slower
I built a project to prove gRPC was faster than HTTP. The results shocked me. Here's a deep dive into how network protocols perform in the real world and why the 'fastest' tool isn't always the winner.
- #dotnet
- #csharp
- #architecture
- #performance
- #messaging
- #docker
My high-performance gRPC service was supposed to be a Formula 1 race car. In my first test against a simple HTTP service, it lost... to a go-kart.
If you've ever been humbled by a piece of code that defied your expectations, you know the feeling. I set out to build a hands-on demo comparing three popular .NET communication methods: a classic HTTP API, a modern gRPC service, and an asynchronous RabbitMQ message queue. The goal was to see their performance differences firsthand. The results weren't what I expected, and they taught me a crucial lesson about performance.
Setting Up the Race: Couriers, Race Cars, and the Postal Service
To make sense of these technologies, I thought of them as different delivery services:
- HTTP API: A standard, reliable courier. You give them a package, they drive it over, get a signature, and come right back. Simple and effective.
- gRPC with Protobuf: A high-tech, private delivery service with a fleet of F1 cars. It uses a super-efficient shorthand language (Protobuf) on a private, multi-lane highway (HTTP/2). It's built for raw speed.
- RabbitMQ: The postal service. You drop a letter in the mailbox and walk away, trusting it will get there. It's not about speed for one letter, but about handling a massive volume of mail without breaking a sweat.
I containerized everything with Docker and built a simple console app to send messages and time the results. I was ready to see the F1 car leave the others in the dust.
// The whole "benchmark": a Stopwatch around each call, timings collected per
// protocol. Crude, but enough to expose a 114 ms outlier — and nothing more.
var timings = new List<long>();
for (var i = 0; i < iterations; i++)
{
var sw = Stopwatch.StartNew();
await client.SendAsync(payload, cancellationToken);
sw.Stop();
timings.Add(sw.ElapsedMilliseconds);
}
Console.WriteLine(
quot;{name,-12} {timings.Count,-8} {timings.Min(),-11} {timings.Max(),-11} {timings.Average():F2}");

The Surprising Results: Why the Go-Kart Won the First Lap
I ran the tests. RabbitMQ was instantly fast, as expected for a 'fire-and-forget' system. But then came the shock: on my local machine, the simple HTTP service consistently beat my powerful gRPC service on average time.
Here's a look at the final summary from my tests:
| Type | Count | Shortest | Longest | Average |
|---|---|---|---|---|
| PROTOBUF | 13 | 1 | 114 | 10.62 |
| RABBITMQ | 15 | 0 | 53 | 3.53 |
| HTTP | 15 | 1 | 19 | 2.20 |
Look at that Longest time for Protobuf: 114ms. That's the key. This is the 'cold start' problem. The F1 car has a huge setup cost: warming up the engine, getting the tires right, and establishing the complex HTTP/2 connection. The go-kart just starts and goes. On a short, local race with zero network latency, the go-kart's simplicity gives it the edge.
Benchmarking Done Right
In retrospect, using a tool like BenchmarkDotNet would have provided even more granular insights, isolating the connection overhead from the actual serialization/deserialization time. For any serious performance testing in .NET, I highly recommend it over manual Stopwatch measurements.
The Real Lesson: Context is Everything
This project was a powerful reminder that in software engineering, the 'best' tool is always relative to the problem you're solving. While gRPC is undoubtedly faster over a real network where its advanced features can shine, the overhead can make it slower in specific, low-latency scenarios.
It's a fantastic insight to have, especially for technical interviews. It shows you don't just know what a technology is; you understand when and why to use it.
If you want to run the race yourself, you can find the complete source code and a guide on how to run it on my GitHub.



